I am developing an app with SwiftUI - using some UIKit components - that has Picture in Picture, and I am trying to keep the picture in a specific corner of the screen when rotating the device. In order to do this I need to change the position of the frame on the view, and so I have registered with UIDevice.orientationDidChangeNotification and when this notification comes through I change the view frame in the UIViewController like this:
#objc func onViewDidTransition() {
view.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.landscapeRight
}
However, this doesn't seem to do anything. The frame doesn't change at all. As though the UIView has kept the old frame and changing to the new CGRect does nothing. When I inspect the object with view.frame. there is no option for x or y, as though these properties are not changeable after initialisation. Is this right? Is there no way for me to change the position of the frame?
EDIT: Code.
The frame is setup in the setup() function of CustomCameraController
func setup() {
view.frame = CGRect(x: 225, y: 0, width: 150, height: 150)
And later it is modified in onViewDidTransition()
#objc func onViewDidTransition() {
view.frame = CGRect(x: 662, y: 0, width: 150, height: 150)
This should put the picture in the top right corner (iphone x) when transitioned to landscape, but it doesn't. The image stays at 200 pt from the left corner.
Minimum reproducible ContentView code
import SwiftUI
import AVFoundation
struct CustomCameraPhotoView: View {
#State private var image: Image?
#State private var showingCustomCamera = false
#State private var showImagePicker = false
#State private var inputImage: UIImage?
#State private var url: URL?
var body: some View {
CustomCameraView(image: self.$inputImage)
}
}
struct CustomCameraView: View {
#Binding var image: UIImage?
#State var didTapCapture: Bool = false
var body: some View {
ZStack() {
CustomCameraRepresentable(image: self.$image, didTapCapture: $didTapCapture)
}
}
}
struct CustomCameraRepresentable: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
#Binding var didTapCapture: Bool
func makeUIViewController(context: Context) -> CustomCameraController {
let controller = CustomCameraController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
if(self.didTapCapture) {
cameraViewController.didTapRecord()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
let parent: CustomCameraRepresentable
init(_ parent: CustomCameraRepresentable) {
self.parent = parent
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
parent.didTapCapture = false
if let imageData = photo.fileDataRepresentation() {
parent.image = UIImage(data: imageData)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
class CustomCameraController: UIViewController {
var image: UIImage?
var captureSession = AVCaptureSession()
var backCamera: AVCaptureDevice?
var frontCamera: AVCaptureDevice?
var currentCamera: AVCaptureDevice?
var photoOutput: AVCapturePhotoOutput?
var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
//DELEGATE
var delegate: AVCapturePhotoCaptureDelegate?
func didTapRecord() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: delegate!)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
view.frame = CGRect(x: 225, y: 0, width: 150, height: 150)
view.layer.cornerRadius = 40
view.layer.masksToBounds = true
view.backgroundColor = .white
setupCaptureSession()
setupDevice()
setupInputOutput()
setupPreviewLayer()
startRunningCaptureSession()
NotificationCenter.default.addObserver(self, selector: #selector(onViewDidTransition), name: UIDevice.orientationDidChangeNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}
#objc func onViewDidTransition() {
view.frame = CGRect(x: 662, y: 0, width: 150, height: 150)
self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.landscapeRight
//UIDevice.current.orientation
}
func setupCaptureSession() {
let cameraMediaType = AVMediaType.video
let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: cameraMediaType)
switch cameraAuthorizationStatus {
case .denied: break
case .authorized: break
case .restricted: break
case .notDetermined:
// Prompting user for the permission to use the camera.
AVCaptureDevice.requestAccess(for: cameraMediaType) { granted in
if granted {
print("Granted access to \(cameraMediaType)")
} else {
print("Denied access to \(cameraMediaType)")
}
}
#unknown default:
break
}
captureSession.sessionPreset = AVCaptureSession.Preset.iFrame1280x720
}
func setupDevice() {
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
mediaType: AVMediaType.video,
position: AVCaptureDevice.Position.front)
for device in deviceDiscoverySession.devices {
switch device.position {
case AVCaptureDevice.Position.front:
self.frontCamera = device
case AVCaptureDevice.Position.back:
self.backCamera = device
default:
break
}
}
self.currentCamera = self.frontCamera
}
func setupInputOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentCamera!)
captureSession.addInput(captureDeviceInput)
photoOutput = AVCapturePhotoOutput()
photoOutput?.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])], completionHandler: nil)
captureSession.addOutput(photoOutput!)
} catch {
print(error)
}
}
func setupPreviewLayer()
{
self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.cameraPreviewLayer?.cornerRadius = 40
self.cameraPreviewLayer?.masksToBounds = true
self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
self.cameraPreviewLayer?.frame = self.view.frame
self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
}
func startRunningCaptureSession(){
captureSession.startRunning()
}
}
Dumb mistake. I was modifying the frame of the View:
view.frame = CGRect(x: 662, y: 0, width: 150, height: 150)
When I should have been looking at the frame of the camera preview layer:
self.cameraPreviewLayer?.frame = self.view.frame
I have changed the code to modify the camera preview layer and leave the view frame alone, and now it works.
self.cameraPreviewLayer?.frame = CGRect(x: 662, y: 0, width: 150, height: 150)
Related
I have these two classes which are for having the custom tapbar, I would like to use them in swiftUI how can I do? I used these wrappers but once implemented the class in the ContentView does not appear to me
I would like to do everything in swiftUI so I would prefer not to use storyboards in the implementation. it's possible ?
//SwiftUI class. I want to use this view already done in SwiftUI
HStack {
SHCircleBarControllerView()
SHCircleBarView()
}
//Class Swift 4
import UIKit
import SwiftUI
struct SHCircleBarControllerView : UIViewControllerRepresentable {
typealias UIViewControllerType = SHCircleBarController
func makeCoordinator() -> SHCircleBarControllerView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<SHCircleBarControllerView>) -> SHCircleBarController {
return SHCircleBarController()
}
func updateUIViewController(_ uiViewController: SHCircleBarController, context: UIViewControllerRepresentableContext<SHCircleBarControllerView>) {
}
class Coordinator : NSObject {
var parent : SHCircleBarControllerView
init(_ viewController : SHCircleBarControllerView){
self.parent = viewController
}
}
}
class SHCircleBarController: UITabBarController {
fileprivate var shouldSelectOnTabBar = true
private var circleView : UIView!
private var circleImageView: UIImageView!
open override var selectedViewController: UIViewController? {
willSet {
guard shouldSelectOnTabBar, let newValue = newValue else {
shouldSelectOnTabBar = true
return
}
guard let tabBar = tabBar as? SHCircleBar, let index = viewControllers?.firstIndex(of: newValue) else {return}
tabBar.select(itemAt: index, animated: true)
}
}
open override var selectedIndex: Int {
willSet {
guard shouldSelectOnTabBar else {
shouldSelectOnTabBar = true
return
}
guard let tabBar = tabBar as? SHCircleBar else {
return
}
tabBar.select(itemAt: selectedIndex, animated: true)
}
}
open override func viewDidLoad() {
super.viewDidLoad()
let tabBar = SHCircleBar()
self.setValue(tabBar, forKey: "tabBar")
self.circleView = UIView(frame: .zero)
circleView.layer.cornerRadius = 30
circleView.backgroundColor = .white
circleView.isUserInteractionEnabled = false
self.circleImageView = UIImageView(frame: .zero)
circleImageView.layer.cornerRadius = 30
circleImageView.isUserInteractionEnabled = false
circleImageView.contentMode = .center
circleView.addSubview(circleImageView)
self.view.addSubview(circleView)
let tabWidth = self.view.bounds.width / CGFloat(self.tabBar.items?.count ?? 4)
circleView.frame = CGRect(x: tabWidth / 2 - 30, y: self.tabBar.frame.origin.y - 40, width: 60, height: 60)
circleImageView.frame = self.circleView.bounds
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
circleImageView.image = image(with: self.tabBar.selectedItem?.image ?? self.tabBar.items?.first?.image, scaledTo: CGSize(width: 30, height: 30))
}
private var _barHeight: CGFloat = 74
open var barHeight: CGFloat {
get {
if #available(iOS 11.0, *) {
return _barHeight + view.safeAreaInsets.bottom
} else {
return _barHeight
}
}
set {
_barHeight = newValue
updateTabBarFrame()
}
}
private func updateTabBarFrame() {
var tabFrame = self.tabBar.frame
tabFrame.size.height = barHeight
tabFrame.origin.y = self.view.frame.size.height - barHeight
self.tabBar.frame = tabFrame
tabBar.setNeedsLayout()
}
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateTabBarFrame()
}
open override func viewSafeAreaInsetsDidChange() {
if #available(iOS 11.0, *) {
super.viewSafeAreaInsetsDidChange()
}
updateTabBarFrame()
}
open override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard let idx = tabBar.items?.firstIndex(of: item) else { return }
if idx != selectedIndex, let controller = viewControllers?[idx] {
shouldSelectOnTabBar = false
selectedIndex = idx
let tabWidth = self.view.bounds.width / CGFloat(self.tabBar.items!.count)
UIView.animate(withDuration: 0.3) {
self.circleView.frame = CGRect(x: (tabWidth * CGFloat(idx) + tabWidth / 2 - 30), y: self.tabBar.frame.origin.y - 15, width: 60, height: 60)
}
UIView.animate(withDuration: 0.15, animations: {
self.circleImageView.alpha = 0
}) { (_) in
self.circleImageView.image = self.image(with: item.image, scaledTo: CGSize(width: 30, height: 30))
UIView.animate(withDuration: 0.15, animations: {
self.circleImageView.alpha = 1
})
}
delegate?.tabBarController?(self, didSelect: controller)
}
}
private func image(with image: UIImage?, scaledTo newSize: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(newSize, _: false, _: 0.0)
image?.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height))
let newImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
You can set up your custom UIViewControllers inside the SHCircleBarController through the viewControllers property.
In SHCircleBarController
open override func viewDidLoad() {
super.viewDidLoad()
...
viewControllers = [ViewController(), ViewController2()]
}
Your other UIViewControllers
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}
class ViewController2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
}
}
This is the result
I have a UIViewController with an UIImageView, if I tap the screen, i want the UIImageView to change its contentMode. But I found out that this is not working with some images, mainly those from AVCaptureSession.
Screenshots:
Aspect fill
Aspect fit
I also found out that it's working fine when I change device orientation to landscape and back. But when I tap the screen is not working again.
Aspect fit after changed orientation to landscape and back (this is how I want it to look everytime in aspect fit)
My code:
CameraController:
class CameraController: UIViewController {
private var captureSession = AVCaptureSession()
private var captureDevice: AVCaptureDevice!
private var capturePhotoOutput = AVCapturePhotoOutput()
private var previewLayer: AVCaptureVideoPreviewLayer!
override func viewDidLoad() {
super.viewDidLoad()
setupCaptureSession()
setupCaptureDevice()
setupInputAndOutput()
setupPreviewLayer()
startCaptureSession()
setupLayout()
}
private func setupCaptureSession() {
captureSession.sessionPreset = .photo
}
private func setupCaptureDevice() {
guard let device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .back).devices.first else { return }
captureDevice = device
}
private func setupInputAndOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: captureDevice)
captureSession.addInput(captureDeviceInput)
let captureSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
capturePhotoOutput.setPreparedPhotoSettingsArray([captureSettings], completionHandler: nil)
captureSession.addOutput(capturePhotoOutput)
} catch {
print(error.localizedDescription)
}
}
private func setupPreviewLayer() {
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.connection?.videoOrientation = .portrait
previewLayer.frame = view.frame
view.layer.insertSublayer(previewLayer, at: 0)
}
private func startCaptureSession() {
captureSession.startRunning()
}
private func setupLayout() {
let captureButton = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
captureButton.backgroundColor = .white
captureButton.layer.cornerRadius = 22
captureButton.addTarget(self, action: #selector(didPressCaptureButton), for: .touchUpInside)
captureButton.center.x = view.center.x
captureButton.center.y = view.frame.height - 50
view.addSubview(captureButton)
}
#objc private func didPressCaptureButton() {
let settings = AVCapturePhotoSettings()
capturePhotoOutput.capturePhoto(with: settings, delegate: self)
}
}
extension CameraController: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let imageData = photo.fileDataRepresentation() else { return }
guard let image = UIImage(data: imageData) else { return }
print("Image size: ", image.size)
let previewController = PreviewController()
previewController.image = image
present(previewController, animated: true, completion: {
self.captureSession.stopRunning()
})
}
}
PreviewController:
class PreviewController: UIViewController {
var imageView: UIImageView!
var image: UIImage!
override func viewDidLoad() {
super.viewDidLoad()
setupImageView()
}
func setupImageView() {
imageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFill
view.addSubview(imageView)
imageView.addConstraintsToFillSuperview()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let contentMode: UIViewContentMode = imageView.contentMode == .scaleAspectFill ? .scaleAspectFit : .scaleAspectFill
imageView.contentMode = contentMode
}
}
What am I doing wrong here?
Thank You!
You can change ImageView contentMode on runtime with:
self.imageView.contentMode = newContentMode
self.imageView.setNeedsDisplay()
I just about have this solved. Thanks to some brilliant help getting me on the right track.
This is the code I have now.
Basically, I can now make an image out of the drawn overlay, and the cameraPreview. But can't yet combine them. There seems very little useful code that I can find that does this simply.
So the important part is the extension block right at the top, and the additions to the
func saveToCamera() near the bottom of the code.
In short, I now have the two images I need, I think. The snap of the myImage is appearing on a white background - so not sure if that's natural - or not. That's how it appears on a Simulator. So it may just be natural.
Image 1. A screen capture.
Image 2. The saved image of myImage as per the explaination.
import UIKit
import AVFoundation
import Foundation
// extension must be outside class
extension UIImage {
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
class ViewController: UIViewController {
#IBOutlet weak var navigationBar: UINavigationBar!
#IBOutlet weak var imgOverlay: UIImageView!
#IBOutlet weak var btnCapture: UIButton!
#IBOutlet weak var shapeLayer: UIView!
let captureSession = AVCaptureSession()
let stillImageOutput = AVCaptureStillImageOutput()
var previewLayer : AVCaptureVideoPreviewLayer?
//var shapeLayer : CALayer?
// If we find a device we'll store it here for later use
var captureDevice : AVCaptureDevice?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//=======================
let midX = self.view.bounds.midX
let midY = self.view.bounds.midY
for index in 1...10 {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: midX,y: midY), radius: CGFloat((index * 10)), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let shapeLayerPath = CAShapeLayer()
shapeLayerPath.path = circlePath.cgPath
//change the fill color
shapeLayerPath.fillColor = UIColor.clear.cgColor
//you can change the stroke color
shapeLayerPath.strokeColor = UIColor.blue.cgColor
//you can change the line width
shapeLayerPath.lineWidth = 0.5
// add the blue-circle layer to the shapeLayer ImageView
shapeLayer.layer.addSublayer(shapeLayerPath)
}
print("Shape layer drawn")
//=====================
captureSession.sessionPreset = AVCaptureSessionPresetHigh
if let devices = AVCaptureDevice.devices() as? [AVCaptureDevice] {
// Loop through all the capture devices on this phone
for device in devices {
// Make sure this particular device supports video
if (device.hasMediaType(AVMediaTypeVideo)) {
// Finally check the position and confirm we've got the back camera
if(device.position == AVCaptureDevicePosition.back) {
captureDevice = device
if captureDevice != nil {
print("Capture device found")
beginSession()
}
}
}
}
}
}
#IBAction func actionCameraCapture(_ sender: AnyObject) {
print("Camera button pressed")
saveToCamera()
}
func beginSession() {
do {
try captureSession.addInput(AVCaptureDeviceInput(device: captureDevice))
stillImageOutput.outputSettings = [AVVideoCodecKey:AVVideoCodecJPEG]
if captureSession.canAddOutput(stillImageOutput) {
captureSession.addOutput(stillImageOutput)
}
}
catch {
print("error: \(error.localizedDescription)")
}
guard let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) else {
print("no preview layer")
return
}
// this is what displays the camera view. But - it's on TOP of the drawn view, and under the overview. ??
self.view.layer.addSublayer(previewLayer)
previewLayer.frame = self.view.layer.frame
captureSession.startRunning()
print("Capture session running")
self.view.addSubview(navigationBar)
//self.view.addSubview(imgOverlay)
self.view.addSubview(btnCapture)
// shapeLayer ImageView is already a subview created in IB
// but this will bring it to the front
self.view.addSubview(shapeLayer)
}
func saveToCamera() {
if let videoConnection = stillImageOutput.connection(withMediaType: AVMediaTypeVideo) {
stillImageOutput.captureStillImageAsynchronously(from: videoConnection, completionHandler: { (CMSampleBuffer, Error) in
if let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(CMSampleBuffer) {
if let cameraImage = UIImage(data: imageData) {
// cameraImage is the camera preview image.
// I need to combine/merge it with the myImage that is actually the blue circles.
// This converts the UIView of the bllue circles to an image. Uses 'extension' at top of code.
let myImage = UIImage(view: self.shapeLayer)
print("converting myImage to an image")
UIImageWriteToSavedPhotosAlbum(cameraImage, nil, nil, nil)
}
}
})
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Give this a try... instead of combining your overlay view, it draws the circles and combines the output:
import UIKit
import AVFoundation
import Foundation
class CameraWithTargetViewController: UIViewController {
#IBOutlet weak var navigationBar: UINavigationBar!
#IBOutlet weak var imgOverlay: UIImageView!
#IBOutlet weak var btnCapture: UIButton!
#IBOutlet weak var shapeLayer: UIView!
let captureSession = AVCaptureSession()
let stillImageOutput = AVCaptureStillImageOutput()
var previewLayer : AVCaptureVideoPreviewLayer?
//var shapeLayer : CALayer?
// If we find a device we'll store it here for later use
var captureDevice : AVCaptureDevice?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//=======================
captureSession.sessionPreset = AVCaptureSessionPresetHigh
if let devices = AVCaptureDevice.devices() as? [AVCaptureDevice] {
// Loop through all the capture devices on this phone
for device in devices {
// Make sure this particular device supports video
if (device.hasMediaType(AVMediaTypeVideo)) {
// Finally check the position and confirm we've got the back camera
if(device.position == AVCaptureDevicePosition.back) {
captureDevice = device
if captureDevice != nil {
print("Capture device found")
beginSession()
}
}
}
}
}
}
#IBAction func actionCameraCapture(_ sender: AnyObject) {
print("Camera button pressed")
saveToCamera()
}
func beginSession() {
do {
try captureSession.addInput(AVCaptureDeviceInput(device: captureDevice))
stillImageOutput.outputSettings = [AVVideoCodecKey:AVVideoCodecJPEG]
if captureSession.canAddOutput(stillImageOutput) {
captureSession.addOutput(stillImageOutput)
}
}
catch {
print("error: \(error.localizedDescription)")
}
guard let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) else {
print("no preview layer")
return
}
// this is what displays the camera view. But - it's on TOP of the drawn view, and under the overview. ??
self.view.layer.addSublayer(previewLayer)
previewLayer.frame = self.view.layer.frame
imgOverlay.frame = self.view.frame
imgOverlay.image = self.drawCirclesOnImage(fromImage: nil, targetSize: imgOverlay.bounds.size)
self.view.bringSubview(toFront: navigationBar)
self.view.bringSubview(toFront: imgOverlay)
self.view.bringSubview(toFront: btnCapture)
// don't use shapeLayer anymore...
// self.view.bringSubview(toFront: shapeLayer)
captureSession.startRunning()
print("Capture session running")
}
func getImageWithColor(color: UIColor, size: CGSize) -> UIImage {
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: size.width, height: size.height))
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
func drawCirclesOnImage(fromImage: UIImage? = nil, targetSize: CGSize? = CGSize.zero) -> UIImage? {
if fromImage == nil && targetSize == CGSize.zero {
return nil
}
var tmpimg: UIImage?
if targetSize == CGSize.zero {
tmpimg = fromImage
} else {
tmpimg = getImageWithColor(color: UIColor.clear, size: targetSize!)
}
guard let img = tmpimg else {
return nil
}
let imageSize = img.size
let scale: CGFloat = 0
UIGraphicsBeginImageContextWithOptions(imageSize, false, scale)
img.draw(at: CGPoint.zero)
let w = imageSize.width
let midX = imageSize.width / 2
let midY = imageSize.height / 2
// red circles - radius in %
let circleRads = [ 0.07, 0.13, 0.17, 0.22, 0.29, 0.36, 0.40, 0.48, 0.60, 0.75 ]
// center "dot" - radius is 1.5%
var circlePath = UIBezierPath(arcCenter: CGPoint(x: midX,y: midY), radius: CGFloat(w * 0.015), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
UIColor.red.setFill()
circlePath.stroke()
circlePath.fill()
// blue circle is between first and second red circles
circlePath = UIBezierPath(arcCenter: CGPoint(x: midX,y: midY), radius: w * CGFloat((circleRads[0] + circleRads[1]) / 2.0), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
UIColor.blue.setStroke()
circlePath.lineWidth = 2.5
circlePath.stroke()
UIColor.red.setStroke()
for pct in circleRads {
let rad = w * CGFloat(pct)
circlePath = UIBezierPath(arcCenter: CGPoint(x: midX, y: midY), radius: CGFloat(rad), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
circlePath.lineWidth = 2.5
circlePath.stroke()
}
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
func saveToCamera() {
if let videoConnection = stillImageOutput.connection(withMediaType: AVMediaTypeVideo) {
stillImageOutput.captureStillImageAsynchronously(from: videoConnection, completionHandler: { (CMSampleBuffer, Error) in
if let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(CMSampleBuffer) {
if let cameraImage = UIImage(data: imageData) {
if let nImage = self.drawCirclesOnImage(fromImage: cameraImage, targetSize: CGSize.zero) {
UIImageWriteToSavedPhotosAlbum(nImage, nil, nil, nil)
}
}
}
})
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Ok, solved it pretty much. The important code is here. The resulting image is slightly out of skew, but I'll work away and fix that, unless someone can see a good fix for it.
func saveToCamera() {
if let videoConnection = stillImageOutput.connection(withMediaType: AVMediaTypeVideo) {
stillImageOutput.captureStillImageAsynchronously(from: videoConnection, completionHandler: { (CMSampleBuffer, Error) in
if let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(CMSampleBuffer) {
if let cameraImage = UIImage(data: imageData) {
// cameraImage is the camera preview image.
// I need to combine/merge it with the myImage that is actually the blue circles.
// This converts the UIView of the bllue circles to an image. Uses 'extension' at top of code.
let myImage = UIImage(view: self.shapeLayer)
print("converting myImage to an image")
let newImage = self.composite(image:cameraImage, overlay:(myImage), scaleOverlay:true)
UIImageWriteToSavedPhotosAlbum(newImage!, nil, nil, nil)
}
}
})
}
}
func composite(image:UIImage, overlay:(UIImage), scaleOverlay: Bool = false)->UIImage?{
UIGraphicsBeginImageContext(image.size)
var rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
image.draw(in: rect)
if scaleOverlay == false {
rect = CGRect(x: 0, y: 0, width: overlay.size.width, height: overlay.size.height)
}
overlay.draw(in: rect)
return UIGraphicsGetImageFromCurrentImageContext()
}
The resulting saved image.
I run into a problem, I'm trying to solve within UIView boundary, is there any way to Add Camera Preview to UIView? And Add other content on top of The UIView (Buttons, Label etc.)?
I try to Using AVFoundation Framework but there is not enough documentation for Swift.
UPDATED TO SWIFT 5
You can try something like this:
import UIKit
import AVFoundation
class ViewController: UIViewController{
var previewView : UIView!
var boxView:UIView!
let myButton: UIButton = UIButton()
//Camera Capture requiered properties
var videoDataOutput: AVCaptureVideoDataOutput!
var videoDataOutputQueue: DispatchQueue!
var previewLayer:AVCaptureVideoPreviewLayer!
var captureDevice : AVCaptureDevice!
let session = AVCaptureSession()
override func viewDidLoad() {
super.viewDidLoad()
previewView = UIView(frame: CGRect(x: 0,
y: 0,
width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
view.addSubview(previewView)
//Add a view on top of the cameras' view
boxView = UIView(frame: self.view.frame)
myButton.frame = CGRect(x: 0, y: 0, width: 200, height: 40)
myButton.backgroundColor = UIColor.red
myButton.layer.masksToBounds = true
myButton.setTitle("press me", for: .normal)
myButton.setTitleColor(UIColor.white, for: .normal)
myButton.layer.cornerRadius = 20.0
myButton.layer.position = CGPoint(x: self.view.frame.width/2, y:200)
myButton.addTarget(self, action: #selector(self.onClickMyButton(sender:)), for: .touchUpInside)
view.addSubview(boxView)
view.addSubview(myButton)
self.setupAVCapture()
}
override var shouldAutorotate: Bool {
if (UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft ||
UIDevice.current.orientation == UIDeviceOrientation.landscapeRight ||
UIDevice.current.orientation == UIDeviceOrientation.unknown) {
return false
}
else {
return true
}
}
#objc func onClickMyButton(sender: UIButton){
print("button pressed")
}
}
// AVCaptureVideoDataOutputSampleBufferDelegate protocol and related methods
extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate{
func setupAVCapture(){
session.sessionPreset = AVCaptureSession.Preset.vga640x480
guard let device = AVCaptureDevice
.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,
for: .video,
position: AVCaptureDevice.Position.back) else {
return
}
captureDevice = device
beginSession()
}
func beginSession(){
var deviceInput: AVCaptureDeviceInput!
do {
deviceInput = try AVCaptureDeviceInput(device: captureDevice)
guard deviceInput != nil else {
print("error: cant get deviceInput")
return
}
if self.session.canAddInput(deviceInput){
self.session.addInput(deviceInput)
}
videoDataOutput = AVCaptureVideoDataOutput()
videoDataOutput.alwaysDiscardsLateVideoFrames=true
videoDataOutputQueue = DispatchQueue(label: "VideoDataOutputQueue")
videoDataOutput.setSampleBufferDelegate(self, queue:self.videoDataOutputQueue)
if session.canAddOutput(self.videoDataOutput){
session.addOutput(self.videoDataOutput)
}
videoDataOutput.connection(with: .video)?.isEnabled = true
previewLayer = AVCaptureVideoPreviewLayer(session: self.session)
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspect
let rootLayer :CALayer = self.previewView.layer
rootLayer.masksToBounds=true
previewLayer.frame = rootLayer.bounds
rootLayer.addSublayer(self.previewLayer)
session.startRunning()
} catch let error as NSError {
deviceInput = nil
print("error: \(error.localizedDescription)")
}
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// do stuff here
}
// clean up AVCapture
func stopCamera(){
session.stopRunning()
}
}
Here i use a UIView called previewView to start the camera and then i add a new UIView called boxView wich is above previewView. I add a UIButton to boxView
IMPORTANT
Remember that in iOS 10 and later you need to first ask the user for permission in order to have access to the camera. You do this by adding a usage
key to your app’s Info.plist together with a purpose string
because if you fail to declare the usage, your app will crash when it
first makes the access.
Here's a screenshot to show the Camera access request
Swift 4
Condensed version of mauricioconde's solution
You can use this as a drop in component:
//
// CameraView.swift
import Foundation
import AVFoundation
import UIKit
final class CameraView: UIView {
private lazy var videoDataOutput: AVCaptureVideoDataOutput = {
let v = AVCaptureVideoDataOutput()
v.alwaysDiscardsLateVideoFrames = true
v.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
v.connection(with: .video)?.isEnabled = true
return v
}()
private let videoDataOutputQueue: DispatchQueue = DispatchQueue(label: "JKVideoDataOutputQueue")
private lazy var previewLayer: AVCaptureVideoPreviewLayer = {
let l = AVCaptureVideoPreviewLayer(session: session)
l.videoGravity = .resizeAspect
return l
}()
private let captureDevice: AVCaptureDevice? = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
private lazy var session: AVCaptureSession = {
let s = AVCaptureSession()
s.sessionPreset = .vga640x480
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
contentMode = .scaleAspectFit
beginSession()
}
private func beginSession() {
do {
guard let captureDevice = captureDevice else {
fatalError("Camera doesn't work on the simulator! You have to test this on an actual device!")
}
let deviceInput = try AVCaptureDeviceInput(device: captureDevice)
if session.canAddInput(deviceInput) {
session.addInput(deviceInput)
}
if session.canAddOutput(videoDataOutput) {
session.addOutput(videoDataOutput)
}
layer.masksToBounds = true
layer.addSublayer(previewLayer)
previewLayer.frame = bounds
session.startRunning()
} catch let error {
debugPrint("\(self.self): \(#function) line: \(#line). \(error.localizedDescription)")
}
}
override func layoutSubviews() {
super.layoutSubviews()
previewLayer.frame = bounds
}
}
extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate {}
iOS 13/14 and Swift 5.3:
private var imageVC: UIImagePickerController?
and then call showCameraVC() when you want to show the camera view
func showCameraVC() {
self.imageVC = UIImagePickerController()
if UIImagePickerController.isCameraDeviceAvailable(.front) {
self.imageVC?.sourceType = .camera
self.imageVC?.cameraDevice = .front
self.imageVC?.showsCameraControls = false
let screenSize = UIScreen.main.bounds.size
let cameraAspectRatio = CGFloat(4.0 / 3.0)
let cameraImageHeight = screenSize.width * cameraAspectRatio
let scale = screenSize.height / cameraImageHeight
self.imageVC?.cameraViewTransform = CGAffineTransform(translationX: 0, y: (screenSize.height - cameraImageHeight)/2)
self.imageVC?.cameraViewTransform = self.imageVC!.cameraViewTransform.scaledBy(x: scale, y: scale)
self.imageVC?.view.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height)
self.view.addSubview(self.imageVC!.view)
self.view.sendSubviewToBack(self.imageVC!.view)
}
}
Camera view will be also fullscreen (other answers wouldn't fix a letterboxed view)
Swift 3:
#IBOutlet weak var cameraContainerView:UIView!
var imagePickers:UIImagePickerController?
On ViewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
addImagePickerToContainerView()
}
Add Camera Preview to the container view:
func addImagePickerToContainerView(){
imagePickers = UIImagePickerController()
if UIImagePickerController.isCameraDeviceAvailable( UIImagePickerControllerCameraDevice.front) {
imagePickers?.delegate = self
imagePickers?.sourceType = UIImagePickerControllerSourceType.camera
//add as a childviewcontroller
addChildViewController(imagePickers!)
// Add the child's View as a subview
self.cameraContainerView.addSubview((imagePickers?.view)!)
imagePickers?.view.frame = cameraContainerView.bounds
imagePickers?.allowsEditing = false
imagePickers?.showsCameraControls = false
imagePickers?.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
On custom button action:
#IBAction func cameraButtonPressed(_ sender: Any) {
if UIImagePickerController.isSourceTypeAvailable(.camera){
imagePickers?.takePicture()
} else{
//Camera not available.
}
}
swift 5
easy way
import UIKit
import AVFoundation
class ViewController: UIViewController, UINavigationControllerDelegate,UIImagePickerControllerDelegate{
//Camera Capture requiered properties
var imagePickers:UIImagePickerController?
#IBOutlet weak var customCameraView: UIView!
override func viewDidLoad() {
addCameraInView()
super.viewDidLoad()
}
func addCameraInView(){
imagePickers = UIImagePickerController()
if UIImagePickerController.isCameraDeviceAvailable( UIImagePickerController.CameraDevice.rear) {
imagePickers?.delegate = self
imagePickers?.sourceType = UIImagePickerController.SourceType.camera
//add as a childviewcontroller
addChild(imagePickers!)
// Add the child's View as a subview
self.customCameraView.addSubview((imagePickers?.view)!)
imagePickers?.view.frame = customCameraView.bounds
imagePickers?.allowsEditing = false
imagePickers?.showsCameraControls = false
imagePickers?.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
#IBAction func cameraButtonPressed(_ sender: Any) {
if UIImagePickerController.isSourceTypeAvailable(.camera){
imagePickers?.takePicture()
} else{
//Camera not available.
}
}
}
I'm trying set camera on the background of UIView in UIViewController, in order to be able to draw on it.
How to do that?
UPDATED TO SWIFT 5
You could try something like this:
I add two UIViews to my UIViewController's main view, one called previewView (for the camera) and another UIView called boxView (which is above the camera view)
class ViewController: UIViewController {
var previewView : UIView!
var boxView:UIView!
//Camera Capture requiered properties
var videoDataOutput: AVCaptureVideoDataOutput!
var videoDataOutputQueue: DispatchQueue!
var previewLayer:AVCaptureVideoPreviewLayer!
var captureDevice : AVCaptureDevice!
let session = AVCaptureSession()
var currentFrame: CIImage!
var done = false
override func viewDidLoad() {
super.viewDidLoad()
previewView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = .scaleAspectFit
view.addSubview(previewView)
//Add a box view
boxView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
boxView.backgroundColor = UIColor.green
boxView.alpha = 0.3
view.addSubview(boxView)
self.setupAVCapture()
}
override func viewWillAppear(_ animated: Bool) {
if !done {
session.startRunning()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override var shouldAutorotate: Bool {
if (UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft ||
UIDevice.current.orientation == UIDeviceOrientation.landscapeRight ||
UIDevice.current.orientation == UIDeviceOrientation.unknown) {
return false
}
else {
return true
}
}
}
// AVCaptureVideoDataOutputSampleBufferDelegate protocol and related methods
extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate{
func setupAVCapture(){
session.sessionPreset = AVCaptureSession.Preset.vga640x480
guard let device = AVCaptureDevice
.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,
for: .video,
position: AVCaptureDevice.Position.front) else{
return
}
captureDevice = device
beginSession()
done = true
}
func beginSession(){
var deviceInput: AVCaptureDeviceInput!
do {
deviceInput = try AVCaptureDeviceInput(device: captureDevice)
guard deviceInput != nil else {
print("error: cant get deviceInput")
return
}
if self.session.canAddInput(deviceInput){
self.session.addInput(deviceInput)
}
videoDataOutput = AVCaptureVideoDataOutput()
videoDataOutput.alwaysDiscardsLateVideoFrames=true
videoDataOutputQueue = DispatchQueue(label: "VideoDataOutputQueue")
videoDataOutput.setSampleBufferDelegate(self, queue:self.videoDataOutputQueue)
if session.canAddOutput(self.videoDataOutput){
session.addOutput(self.videoDataOutput)
}
videoDataOutput.connection(with: AVMediaType.video)?.isEnabled = true
self.previewLayer = AVCaptureVideoPreviewLayer(session: self.session)
self.previewLayer.videoGravity = AVLayerVideoGravity.resizeAspect
let rootLayer: CALayer = self.previewView.layer
rootLayer.masksToBounds = true
self.previewLayer.frame = rootLayer.bounds
rootLayer.addSublayer(self.previewLayer)
session.startRunning()
} catch let error as NSError {
deviceInput = nil
print("error: \(error.localizedDescription)")
}
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
currentFrame = self.convertImageFromCMSampleBufferRef(sampleBuffer)
}
// clean up AVCapture
func stopCamera(){
session.stopRunning()
done = false
}
func convertImageFromCMSampleBufferRef(_ sampleBuffer:CMSampleBuffer) -> CIImage{
let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
let ciImage:CIImage = CIImage(cvImageBuffer: pixelBuffer)
return ciImage
}
}
You can replace the boxView's frame with mainView's frameand don't set its background property. This way you can use this view to add more subviews.
IMPORTANT
Remember that in iOS 10 you need to first ask the user for permission in order to have access to the camera. You do this by adding a usage
key to your app’s Info.plist together with a purpose string
because if you fail to declare the usage, your app will crash when it
first makes the access.
Here's a screenshot to show the Camera access request
I hope this can help!
An other way, SceneView is useful for augmented reality applications.
Create a preview layer with AVFramework or UIView, then add preview
layer to view's sublayer.
Create and custumize a sceneview. Then add sceneview to view's
subview.
Create and custimize scene. Finally add to scenview's scene.
// 1. Create a preview layer with AVFramework or UIView, then add preview layer to view's sublayer.
self.previewLayer!.frame = view.layer.bounds
view.clipsToBounds = true
view.layer.addSublayer(self.previewLayer!)
// 2. Create and custumize a sceneview. Then add sceneview to view's subview.
let sceneView = SCNView()
sceneView.frame = view.bounds
sceneView.backgroundColor = UIColor.clearColor()
self.previewLayer!.frame = view.bounds
view.addSubview(sceneView)
// 3 . Create and custimize scene. Finally add to scenview's scene.
let scene = SCNScene()
sceneView.autoenablesDefaultLighting = true
sceneView.allowsCameraControl = true
let boxGeometry = SCNBox(width: 800 , height: 400, length: 1.0, chamferRadius: 1.0)
let yellow = UIColor.yellowColor()
let semi = yellow.colorWithAlphaComponent(0.3)
boxGeometry.firstMaterial?.diffuse.contents = semi
let boxNode = SCNNode(geometry: boxGeometry)
scene.rootNode.addChildNode(boxNode)
sceneView.scene = scene
One easy way of doing this is to add overlay view on imagepickercontroller and hide the default view.
The other way is to use AV framework that will give you much more options and freedom.
Choice depends on your needs.