I am using webRTC for video calling. Everything is running smooth but
I am struggling with aspect ratio of Remote video on iPhoneX, XSMax. I am seeing lot of zoom in video. Can you please help me out how I can manage remote video on devices that have notch. Below is the code where I am handling remote size.
func videoView(_ videoView: RTCEAGLVideoView, didChangeVideoSize size: CGSize) {
print(size)
let defaultAspectRatio: CGSize = CGSize(width: 4, height: 3)
let aspectRatio: CGSize = size.equalTo(CGSize.zero) ? defaultAspectRatio : size
let videoRect: CGRect = self.view.bounds
let maxFloat = CGFloat.maximum(self.view.frame.width, self.view.frame.height)
let newAspectRatio = aspectRatio.width / aspectRatio.height
var frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
if (aspectRatio.width < aspectRatio.height) {
frame.size.width = maxFloat;
frame.size.height = frame.size.width / newAspectRatio;
} else {
frame.size.height = maxFloat;
frame.size.width = frame.size.height * newAspectRatio;
}
frame.origin.x = (self.view.frame.width - frame.size.width) / 2
frame.origin.y = (self.view.frame.height - frame.size.height) / 2
self.remoteView.frame = frame
}
According to #Eysner's answer, what is work for me, the final code (written using swift 5):
import UIKit
import WebRTC
final class WebRTCView: UIView, RTCVideoViewDelegate {
let videoView = RTCEAGLVideoView(frame: .zero)
var videoSize = CGSize.zero
override init(frame: CGRect) {
super.init(frame: frame)
videoView.delegate = self
addSubview(videoView)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
videoView.delegate = self
addSubview(videoView)
}
func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
self.videoSize = size
setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
guard videoSize.width > 0 && videoSize.height > 0 else {
videoView.frame = bounds
return
}
var videoFrame = AVMakeRect(aspectRatio: videoSize, insideRect: bounds)
let scale = videoFrame.size.aspectFitScale(in: bounds.size)
videoFrame.size.width = videoFrame.size.width * CGFloat(scale)
videoFrame.size.height = videoFrame.size.height * CGFloat(scale)
videoView.frame = videoFrame
videoView.center = CGPoint(x: bounds.midX, y: bounds.midY)
}
}
extension CGSize {
func aspectFitScale(in container: CGSize) -> CGFloat {
if height <= container.height && width > container.width {
return container.width / width
}
if height > container.height && width > container.width {
return min(container.width / width, container.height / height)
}
if height > container.height && width <= container.width {
return container.height / height
}
if height <= container.height && width <= container.width {
return min(container.width / width, container.height / height)
}
return 1.0
}
}
There is aspectFitScale function, it is simply to describe logic, you can refactor it if you want to.
I'm using this approach in the demo:
https://github.com/Maxatma/Walkie-Talkie/
You can make some class with show video -
class SomeVideoView: UIView, RTCVideoViewDelegate {
let videoView = RTCEAGLVideoView(frame: .zero)
var videoSize = CGSize.zero
With init method
override init(frame: CGRect) {
super.init(frame: frame)
videoView.delegate = self
self.addSubview(videoView)
...
Handle delegate method videoView:didChangeVideoSize like this (we don't need change frame in UI, only mark needsLayout)
func videoView(_ videoView: RTCEAGLVideoView, didChangeVideoSize size: CGSize) {
if (self.videoView == videoView) {
self.videoSize = size
}
self.setNeedsLayout()
}
And overwrite layoutSubviews method
override func layoutSubviews() {
if (self.videoSize.width > 0 && self.videoSize.height > 0) {
var videoFrame = AVMakeRect(aspectRatio: self.videoSize, insideRect: bounds)
var scale = 1.0
if (videoFrame.size.width > videoFrame.size.height) {
scale = bounds.size.height / videoFrame.size.height
} else {
scale = bounds.size.width / videoFrame.size.width
}
videoFrame.size.width = videoFrame.size.width * scale
videoFrame.size.height = videoFrame.size.height * scale
self.videoView.frame = videoFrame
self.videoView.center = CGPointMake(bounds.midX, bounds.midY)
} else {
self.videoView.frame = bounds
}
...
}
And set up SomeVideoView is full screen (this link can be help with it)
Safe Area of Xcode 9
just simple using AVMakeRect
//TODO: Default mobile
if size.width < size.height{
newSize = aspectFill(aspectRatio: size, minimumSize: UIScreen.main.bounds.size)
}else{
//Default computer
newSize = AVMakeRect(aspectRatio: size, insideRect: UIScreen.main.bounds).size
}
for sometimes user may have call from website, so i didnt make fill from it
func aspectFill(aspectRatio : CGSize, minimumSize: CGSize) -> CGSize {
let mW = minimumSize.width / aspectRatio.width;
let mH = minimumSize.height / aspectRatio.height;
var temp = minimumSize
if( mH > mW ) {
temp.width = minimumSize.height / aspectRatio.height * aspectRatio.width;
}
else if( mW > mH ) {
temp.height = minimumSize.width / aspectRatio.width * aspectRatio.height;
}
return temp;
}
I'm currently overlaying a UIView when a button is pressed on top of a viewController. I have it positioned well in other screen devices and when i run in on iPhone X + devices. Its not positioned well. For it to position it well, I need to subtract 40 points to its current top safeAreaInsets. The initial 20 points is to cope with the status bar. When I do this it works and it then doesn't work on the other iPhones that don't have a notch. What I'm doing wrong here. Here is my code
func contentFrame(extraTopOffset pOffset: CGFloat = 0) -> CGRect {
var theFrame = self.view.bounds
if let navBar = self.navigationBar {
let topOffset = navBar.frame.origin.y + navBar.frame.size.height
theFrame.size.height -= topOffset
theFrame.origin.y += topOffset
}
if #available(iOS 11.0, *) {
let safeInsets = UIApplication.shared.delegate!.window!?.safeAreaInsets
let topOffset = safeInsets!.top - (20 + pOffset)
theFrame.origin.y += topOffset
theFrame.size.height -= topOffset + safeInsets!.bottom
}
return theFrame
}
Where I show the overlay in another viewController where I override the method
override func showOverlay() {
self.overlayView.frame = self.contentFrame()
self.view.addSubview(self.overlayView)
}
I fixed it by using my implementation. I check if current device has a notch and If it does, I subtract 20 from it. Then add 20 points when I call the method
class var hasNotch: Bool {
if #available(iOS 11.0, *) {
return (self.shared.delegate!.window!!.safeAreaInsets.top > 20)
}
return false
}
func contentFrame(extraTopOffset pOffset: CGFloat = 0) -> CGRect {
var theFrame = self.view.bounds
if let navBar = self.navigationBar {
let topOffset = navBar.frame.origin.y + navBar.frame.size.height
theFrame.size.height -= topOffset
theFrame.origin.y += topOffset
}
if #available(iOS 11.0, *) {
let safeInsets = UIApplication.shared.delegate!.window!?.safeAreaInsets
var extraOffset = safeInsets!.top - 20
if UIApplication.yb_hasNotch {
extraOffset -= pOffset
}
theFrame.origin.y += extraOffset
theFrame.size.height -= extraOffset + safeInsets!.bottom
}
return theFrame
}
You can use an extension like this.
extension UIDevice {
var hasNotch: Bool {
if #available(iOS 11.0, *) {
let bottom = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
return bottom > 0
} else {
return false
}
}
}
I've got a view controller being pushed onto a navigation controller which is inside a tab bar controller. The view controller hidesBottomBarWhenPushed and on view will appear it shows a toolbar. No matter what I try, it won't stop animating the toolbar sliding up / down when the view controller is pushed or popped. This only seems to be an issue on the iPhone X. Does anyone know how work around it?
This answer https://stackoverflow.com/a/47225653/1211917 helps me:
class SafeAreaFixTabBar: UITabBar {
var oldSafeAreaInsets = UIEdgeInsets.zero
#available(iOS 11.0, *)
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
if oldSafeAreaInsets != safeAreaInsets {
oldSafeAreaInsets = safeAreaInsets
invalidateIntrinsicContentSize()
superview?.setNeedsLayout()
superview?.layoutSubviews()
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
let bottomInset = safeAreaInsets.bottom
if bottomInset > 0 && size.height < 50 && (size.height + bottomInset < 90) {
size.height += bottomInset
}
}
return size
}
override var frame: CGRect {
get {
return super.frame
}
set {
var tmp = newValue
if let superview = superview, tmp.maxY !=
superview.frame.height {
tmp.origin.y = superview.frame.height - tmp.height
}
super.frame = tmp
}
}
}
I'm using this code to make a custom camera crop:
UIImagePickerController editing view circle overlay
This works perfectly in camera roll but not taking photos
If I change [navigationController.viewControllers count] == 3 --> [navigationController.viewControllers count] == 1 works for camera too, but not in next view (preview view where you accept to use the photo)
Someone help me??
-(void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{
if (buttonIndex == 0) {
NSLog(#"Camara");
UIImagePickerController * imagePicker = [[UIImagePickerController alloc] init];
imagePicker.allowsEditing = YES;
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
imagePicker.delegate = self;
self.isCamera = YES;
[self presentViewController:imagePicker animated:YES completion:nil];
}else{
NSLog(#"Carrete");
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc]init];
imagePickerController.allowsEditing = YES;
imagePickerController.delegate = self;
imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
self.isCamera = NO;
[self presentViewController:imagePickerController animated:YES completion:nil];
}
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
if (self.isCamera) {
if ([navigationController.viewControllers count] == 1)
{
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
UIView *plCropOverlay = [[[viewController.view.subviews objectAtIndex:1]subviews] objectAtIndex:0];
plCropOverlay.hidden = YES;
int position = 0;
if (screenHeight == 568)
{
position = 124;
}
else
{
position = 80;
}
CAShapeLayer *circleLayer = [CAShapeLayer layer];
UIBezierPath *path2 = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, position, 320.0f, 320.0f)];
[path2 setUsesEvenOddFillRule:YES];
[circleLayer setPath:[path2 CGPath]];
[circleLayer setFillColor:[[UIColor clearColor] CGColor]];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 320, screenHeight-72) cornerRadius:0];
[path appendPath:path2];
[path setUsesEvenOddFillRule:YES];
CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor blackColor].CGColor;
fillLayer.opacity = 0.8;
[viewController.view.layer addSublayer:fillLayer];
UILabel *moveLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, 320, 50)];
[moveLabel setText:#"Move and Scale"];
[moveLabel setTextAlignment:NSTextAlignmentCenter];
[moveLabel setTextColor:[UIColor whiteColor]];
[viewController.view addSubview:moveLabel];
}
}else{
if ([navigationController.viewControllers count] == 3)
{
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
UIView *plCropOverlay = [[[viewController.view.subviews objectAtIndex:1]subviews] objectAtIndex:0];
plCropOverlay.hidden = YES;
int position = 0;
if (screenHeight == 568)
{
position = 124;
}
else
{
position = 80;
}
CAShapeLayer *circleLayer = [CAShapeLayer layer];
UIBezierPath *path2 = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, position, 320.0f, 320.0f)];
[path2 setUsesEvenOddFillRule:YES];
[circleLayer setPath:[path2 CGPath]];
[circleLayer setFillColor:[[UIColor clearColor] CGColor]];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 320, screenHeight-72) cornerRadius:0];
[path appendPath:path2];
[path setUsesEvenOddFillRule:YES];
CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor blackColor].CGColor;
fillLayer.opacity = 0.8;
[viewController.view.layer addSublayer:fillLayer];
UILabel *moveLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, 320, 50)];
[moveLabel setText:#"Move and Scale"];
[moveLabel setTextAlignment:NSTextAlignmentCenter];
[moveLabel setTextColor:[UIColor whiteColor]];
[viewController.view addSubview:moveLabel];
}
}
}
Similar solution in SwiftUI, Swift 5
I banged my head on this for a long, long time, but finally have a solution that works.
I've not been coding in Swift or SwiftUI for long, and I absolutely welcome comments to improve this code. In some places, I've left in a bit of Debugging code you can uncomment. Wrapping my head around the math involved more trial and error than competent, well-thought out approaches!
Shortcomings in this code: first, it would be nice to open the Impage picker from ContentView() and then show my custom view. I don't know how to do that. Second, if there is already an image in the ContentView(), it would be nice to populate the image in the custom view. However, then the user may expect to be able to get the "original" image and move it and scale it. That would require more than is needed for this answer. Ie, do you want to save the original photo in some url / application folder as well as the cropped version? Or even save a dictionary with the original picture and the CGRect needed to recreate the cropped view? Third. there seems to be a bug if the selected photo is exactly the size of the screen (a screenshot); it can be scaled too low.
In a new SwiftUI lifecycle app, I have the following SwiftUI views:
ContentView with a UIImage binding to the subsequent
ImageMoveAndScaleSheet with a further UIBinding to
ImagePicker which is the UIViewControllerRepresentable from Hacking with Swift found here: https://www.hackingwithswift.com/books/ios-swiftui/importing-an-image-into-swiftui-using-uiimagepickercontroller
This is what you'll get:
I also use this crucial solution for cropping:
ImageManipulation.swift
Finally, some of my code accesses system UIcolors so I use the extension in
Colors.swift
ContentView
import SwiftUI
struct ContentView: View {
#State private var isShowingPhotoSelectionSheet = false
#State private var finalImage: UIImage?
#State private var inputImage: UIImage?
var body: some View {
VStack {
if finalImage != nil {
Image(uiImage: finalImage!)
.resizable()
.frame(width: 100, height: 100)
.scaledToFill()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.shadow(radius: 4)
} else {
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.aspectRatio(contentMode: .fit)
.foregroundColor(.systemGray2)
}
Button (action: {
self.isShowingPhotoSelectionSheet = true
}, label: {
Text("Change photo")
.foregroundColor(.systemRed)
.font(.footnote)
})
}
.background(Color.systemBackground)
.statusBar(hidden: isShowingPhotoSelectionSheet)
.fullScreenCover(isPresented: $isShowingPhotoSelectionSheet, onDismiss: loadImage) {
ImageMoveAndScaleSheet(croppedImage: $finalImage)
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
finalImage = inputImage
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Clicking / tapping on Change photo brings up the next view:
ImagemoveAndScaleSheet
This is a fullscreen modal which hides the statusbar while open.
import SwiftUI
struct ImageMoveAndScaleSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var isShowingImagePicker = false
///The croped image is what will will send back to the parent view.
///It should be the part of the image in the square defined by the
///cutout circle's diamter. See below, the cutout circle has an "inset" value
///which can be changed.
#Binding var croppedImage: UIImage?
///The input image is received from the ImagePicker.
///We will need to calculate and refer to its aspectr ratio in the functions.
#State private var inputImage: UIImage?
#State private var inputW: CGFloat = 750.5556577
#State private var inputH: CGFloat = 1336.5556577
#State private var theAspectRatio: CGFloat = 0.0
///The profileImage is what wee see on this view. When added from the
///ImapgePicker, it will be sized to fit the screen,
///meaning either its width will match the width of the device's screen,
///or its height will match the height of the device screen.
///This is not suitable for landscape mode or for iPads.
#State private var profileImage: Image?
#State private var profileW: CGFloat = 0.0
#State private var profileH: CGFloat = 0.0
///Zoom and Drag ...
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
///We track of amount the image is moved for use in functions below.
#State private var horizontalOffset: CGFloat = 0.0
#State private var verticalOffset: CGFloat = 0.0
var body: some View {
ZStack {
ZStack {
Color.black.opacity(0.8)
if profileImage != nil {
profileImage?
.resizable()
.scaleEffect(finalAmount + currentAmount)
.scaledToFill()
.aspectRatio(contentMode: .fit)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
} else {
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaleEffect(finalAmount + currentAmount)
.scaledToFill()
.aspectRatio(contentMode: .fit)
.foregroundColor(.systemGray2)
}
}
Rectangle()
.fill(Color.black).opacity(0.55)
.mask(HoleShapeMask().fill(style: FillStyle(eoFill: true)))
VStack {
Text((profileImage != nil) ? "Move and Scale" : "Select a Photo by tapping the icon below")
.foregroundColor(.white)
.padding(.top, 50)
Spacer()
HStack{
ZStack {
HStack {
Button(
action: {presentationMode.wrappedValue.dismiss()},
label: { Text("Cancel") })
Spacer()
Button(
action: {
self.save()
presentationMode.wrappedValue.dismiss()
})
{ Text("Save") }
.opacity((profileImage != nil) ? 1.0 : 0.2)
.disabled((profileImage != nil) ? false: true)
}
.padding(.horizontal)
.foregroundColor(.white)
Image(systemName: "circle.fill")
.font(.custom("system", size: 45))
.opacity(0.9)
.foregroundColor(.white)
Image(systemName: "photo.on.rectangle")
.imageScale(.medium)
.foregroundColor(.black)
.onTapGesture {
isShowingImagePicker = true
}
}
.padding(.bottom, 5)
}
}
.padding()
}
.edgesIgnoringSafeArea(.all)
//MARK: - Gestures
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
// repositionImage()
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
repositionImage()
}
)
.simultaneousGesture(
DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.newPosition = self.currentPosition
repositionImage()
}
)
.simultaneousGesture(
TapGesture(count: 2)
.onEnded({
resetImageOriginAndScale()
})
)
.sheet(isPresented: $isShowingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
.accentColor(Color.systemRed)
}
}
//MARK: - functions
private func HoleShapeMask() -> Path {
let rect = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
let insetRect = CGRect(x: inset, y: inset, width: UIScreen.main.bounds.width - ( inset * 2 ), height: UIScreen.main.bounds.height - ( inset * 2 ))
var shape = Rectangle().path(in: rect)
shape.addPath(Circle().path(in: insetRect))
return shape
}
///Called when the ImagePicker is dismissed.
///We want to measure the image receoived and determine the aspect ratio.
private func loadImage() {
guard let inputImage = inputImage else { return }
let w = inputImage.size.width
let h = inputImage.size.height
profileImage = Image(uiImage: inputImage)
inputW = w
inputH = h
theAspectRatio = w / h
resetImageOriginAndScale()
}
///The profileImage will size to fit the screen.
///But we need to know the width and height
///to set the related #State variables.
///Douobke-tpping the image will also set it
///as it was sized originally upon loading.
private func resetImageOriginAndScale() {
withAnimation(.easeInOut){
if theAspectRatio >= screenAspect {
profileW = UIScreen.main.bounds.width
profileH = profileW / theAspectRatio
} else {
profileH = UIScreen.main.bounds.height
profileW = profileH * theAspectRatio
}
currentAmount = 0
finalAmount = 1
currentPosition = .zero
newPosition = .zero
}
}
private func repositionImage() {
//Screen width
let w = UIScreen.main.bounds.width
if theAspectRatio > screenAspect {
profileW = UIScreen.main.bounds.width * finalAmount
profileH = profileW / theAspectRatio
} else {
profileH = UIScreen.main.bounds.height * finalAmount
profileW = profileH * theAspectRatio
}
horizontalOffset = (profileW - w ) / 2
verticalOffset = ( profileH - w ) / 2
///Keep the user from zooming too far in. Adjust as required by the individual project.
if finalAmount > 4.0 {
withAnimation{
finalAmount = 4.0
}
}
///The following if statements keep the image filling the circle cutout.
if profileW >= UIScreen.main.bounds.width {
if newPosition.width > horizontalOffset {
withAnimation(.easeInOut) {
newPosition = CGSize(width: horizontalOffset + inset, height: newPosition.height)
currentPosition = CGSize(width: horizontalOffset + inset, height: currentPosition.height)
}
}
if newPosition.width < ( horizontalOffset * -1) {
withAnimation(.easeInOut){
newPosition = CGSize(width: ( horizontalOffset * -1) - inset, height: newPosition.height)
currentPosition = CGSize(width: ( horizontalOffset * -1 - inset), height: currentPosition.height)
}
}
} else {
withAnimation(.easeInOut) {
newPosition = CGSize(width: 0, height: newPosition.height)
currentPosition = CGSize(width: 0, height: newPosition.height)
}
}
if profileH >= UIScreen.main.bounds.width {
if newPosition.height > verticalOffset {
withAnimation(.easeInOut){
newPosition = CGSize(width: newPosition.width, height: verticalOffset + inset)
currentPosition = CGSize(width: newPosition.width, height: verticalOffset + inset)
}
}
if newPosition.height < ( verticalOffset * -1) {
withAnimation(.easeInOut){
newPosition = CGSize(width: newPosition.width, height: ( verticalOffset * -1) - inset)
currentPosition = CGSize(width: newPosition.width, height: ( verticalOffset * -1) - inset)
}
}
} else {
withAnimation (.easeInOut){
newPosition = CGSize(width: newPosition.width, height: 0)
currentPosition = CGSize(width: newPosition.width, height: 0)
}
}
if profileW <= UIScreen.main.bounds.width && theAspectRatio > screenAspect {
resetImageOriginAndScale()
}
if profileH <= UIScreen.main.bounds.height && theAspectRatio < screenAspect {
resetImageOriginAndScale()
}
}
private func save() {
let scale = (inputImage?.size.width)! / profileW
let xPos = ( ( ( profileW - UIScreen.main.bounds.width ) / 2 ) + inset + ( currentPosition.width * -1 ) ) * scale
let yPos = ( ( ( profileH - UIScreen.main.bounds.width ) / 2 ) + inset + ( currentPosition.height * -1 ) ) * scale
let radius = ( UIScreen.main.bounds.width - inset * 2 ) * scale
croppedImage = imageWithImage(image: inputImage!, croppedTo: CGRect(x: xPos, y: yPos, width: radius, height: radius))
///Debug maths
print("Input: w \(inputW) h \(inputH)")
print("Profile: w \(profileW) h \(profileH)")
print("X Origin: \( ( ( profileW - UIScreen.main.bounds.width - inset ) / 2 ) + ( currentPosition.width * -1 ) )")
print("Y Origin: \( ( ( profileH - UIScreen.main.bounds.width - inset) / 2 ) + ( currentPosition.height * -1 ) )")
print("Scale: \(scale)")
print("Profile:\(profileW) + \(profileH)" )
print("Curent Pos: \(currentPosition.debugDescription)")
print("Radius: \(radius)")
print("x:\(xPos), y:\(yPos)")
}
let inset: CGFloat = 15
let screenAspect = UIScreen.main.bounds.width / UIScreen.main.bounds.height
}
Apart from the drag and scale gestures, the main things to look and (and probably clean up!) are the functions.
HoleShapeMask() (cannot remember where that code is, but I know I got it on SO.
repositionImage() (much headbanging here)
save() which uses the funciton in the ImageManipulation.swift file.
ImagePicker
Again, this is simply from Hacking With Swift. (Thanks Paul!) https://twitter.com/twostraws/
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
ImageManipulation.swift
This contains the following code:
import UIKit
func imageWithImage(image: UIImage, croppedTo rect: CGRect) -> UIImage {
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
let drawRect = CGRect(x: -rect.origin.x, y: -rect.origin.y,
width: image.size.width, height: image.size.height)
context?.clip(to: CGRect(x: 0, y: 0,
width: rect.size.width, height: rect.size.height))
image.draw(in: drawRect)
let subImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return subImage!
}
## Colors.swift ##
A handy extension to access system UIColors in SwiftUI:
import Foundation
import SwiftUI
extension Color {
static var label: Color {
return Color(UIColor.label)
}
static var secondaryLabel: Color {
return Color(UIColor.secondaryLabel)
}
static var tertiaryLabel: Color {
return Color(UIColor.tertiaryLabel)
}
static var quaternaryLabel: Color {
return Color(UIColor.quaternaryLabel)
}
static var systemFill: Color {
return Color(UIColor.systemFill)
}
static var secondarySystemFill: Color {
return Color(UIColor.secondarySystemFill)
}
static var tertiarySystemFill: Color {
return Color(UIColor.tertiarySystemFill)
}
static var quaternarySystemFill: Color {
return Color(UIColor.quaternarySystemFill)
}
static var systemBackground: Color {
return Color(UIColor.systemBackground)
}
static var secondarySystemBackground: Color {
return Color(UIColor.secondarySystemBackground)
}
static var tertiarySystemBackground: Color {
return Color(UIColor.tertiarySystemBackground)
}
static var systemGroupedBackground: Color {
return Color(UIColor.systemGroupedBackground)
}
static var secondarySystemGroupedBackground: Color {
return Color(UIColor.secondarySystemGroupedBackground)
}
static var tertiarySystemGroupedBackground: Color {
return Color(UIColor.tertiarySystemGroupedBackground)
}
static var systemRed: Color {
return Color(UIColor.systemRed)
}
static var systemBlue: Color {
return Color(UIColor.systemBlue)
}
static var systemPink: Color {
return Color(UIColor.systemPink)
}
static var systemTeal: Color {
return Color(UIColor.systemTeal)
}
static var systemGreen: Color {
return Color(UIColor.systemGreen)
}
static var systemIndigo: Color {
return Color(UIColor.systemIndigo)
}
static var systemOrange: Color {
return Color(UIColor.systemOrange)
}
static var systemPurple: Color {
return Color(UIColor.systemPurple)
}
static var systemYellow: Color {
return Color(UIColor.systemYellow)
}
static var systemGray: Color {
return Color(UIColor.systemGray)
}
static var systemGray2: Color {
return Color(UIColor.systemGray2)
}
static var systemGray3: Color {
return Color(UIColor.systemGray3)
}
static var systemGray4: Color {
return Color(UIColor.systemGray4)
}
static var systemGray5: Color {
return Color(UIColor.systemGray5)
}
static var systemGray6: Color {
return Color(UIColor.systemGray6)
}
}
extension ProfileViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard imagePickerController?.sourceType == .camera else {
return
}
guard let view = viewController.view.subviews(deep: true, where: {
String(describing: type(of:$0)) == "CAMPreviewView"
}).first else {
return
}
viewController.view.layoutIfNeeded()
let camPreviewBounds = view.bounds
let circleRect = CGRect(
x: camPreviewBounds.minX + (camPreviewBounds.width - 320) * 0.5,
y: camPreviewBounds.minY + (camPreviewBounds.height - 320) * 0.5,
width: 320,
height: 320
)
let path = UIBezierPath(roundedRect: camPreviewBounds, cornerRadius: 0)
path.append(UIBezierPath(ovalIn: circleRect))
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillRule = CAShapeLayerFillRule.evenOdd;
layer.fillColor = UIColor.black.cgColor
layer.opacity = 0.8;
view.layer.addSublayer(layer)
}
}
Although I believe that my reply might be too late, I ended it up mixing my solution with this one: https://gist.github.com/hamin/e8c6dfe00d9c81375f3e, where:
In order to get overlay working properly on Camera, I was listening to notifications (taken & rejecting picture) due to add or remove circle overlay
Kept the solution mentioned above where I need to loop through UINavigationController and draw circle overlay when it was requested to.
To sum up, please find below my solution written in Swift:
public class CustomPicture: NSObject {
//MARK: - Properties
private var myPickerController: UIImagePickerController?
private var plCropOverlayBottomBar: UIView?
private var customLayer: CAShapeLayer?
//MARK: - Constants
private let screenHeight = UIScreen.mainScreen().bounds.size.height
private let screenWidth = UIScreen.mainScreen().bounds.size.width
private let kCameraNotificationIrisAnimationEnd = "_UIImagePickerControllerUserDidCaptureItem"
private let kCameraNotificationUserRejection = "_UIImagePickerControllerUserDidRejectItem"
private let kPUUIImageViewController = "PUUIImageViewController"
private let kPLUIImageViewController = "PLUIImageViewController"
private let kPLCropOverlayCropView = "PLCropOverlayCropView"
private let kPLCropOverlayBottomBar = "PLCropOverlayBottomBar"
//MARK: - Overrides
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
//MARK: - Privates
private func camera() {
listenToCameraNotifications()
let myPickerController = UIImagePickerController()
myPickerController.delegate = self
myPickerController.sourceType = .Camera
myPickerController.allowsEditing = true
self.myPickerController = myPickerController
self.navigationController?.presentViewController(myPickerController, animated: true, completion: nil)
}
private func listenToCameraNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(cameraNotificationIrisEnd), name: kCameraNotificationIrisAnimationEnd, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(cameraNotificationRejected), name: kCameraNotificationUserRejection, object: nil)
}
private func photoLibrary() {
let myPickerController = UIImagePickerController()
myPickerController.delegate = self
myPickerController.allowsEditing = true
myPickerController.sourceType = .PhotoLibrary
self.myPickerController = myPickerController
self.navigationController?.presentViewController(myPickerController, animated: true, completion: nil)
}
//MARK: - Selector
/**
Listen to notification sent after reject button has been touched
*/
func cameraNotificationRejected() {
customLayer!.removeFromSuperlayer()
plCropOverlayBottomBar!.removeFromSuperview()
}
/**
Listen to notification sent after picture has been taken
*/
func cameraNotificationIrisEnd() {
addCircleOverlay(viewController: self.myPickerController!)
}
}
extension CustomPicture: UINavigationControllerDelegate {
//MARK: - Override
public func navigationController(navigationController: UINavigationController, willShowViewController: UIViewController, animated: Bool) {
if isImageViewer(navigationController: navigationController) {
addCircleOverlay(viewController: willShowViewController)
}
}
//MARK: - Private
private func addCircleOverlay(viewController viewController: UIViewController) {
hidePLCropOverlay(view: viewController.view)
setPLCropOverlayBottomBar(view: viewController.view)
setCustomLayer(viewController: viewController)
}
private func getCirclePath() -> UIBezierPath {
let circlePath = UIBezierPath(ovalInRect: CGRectMake(0, screenHeight / 2 - screenWidth / 2, screenWidth, screenWidth))
circlePath.usesEvenOddFillRule = true
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.clearColor().CGColor
return circlePath
}
private func getMaskPath(screenWidth screenWidth: CGFloat, screenHeight: CGFloat, circlePath: UIBezierPath) -> UIBezierPath {
let maskPath = UIBezierPath(roundedRect: CGRectMake(0, 0, screenWidth, screenHeight), cornerRadius: 0)
maskPath.appendPath(circlePath)
maskPath.usesEvenOddFillRule = true
return maskPath
}
private func hidePLCropOverlay(view view: UIView) {
for myView in view.subviews {
if myView.isKindOfClass(NSClassFromString(kPLCropOverlayCropView)!) {
myView.hidden = true
break
} else {
hidePLCropOverlay(view: myView as UIView)
}
}
}
private func isImageViewer(navigationController navigationController: UINavigationController) -> Bool {
if (navigationController.viewControllers.count == 3 &&
(navigationController.viewControllers[2].dynamicType.description() == kPUUIImageViewController ||
navigationController.viewControllers[2].dynamicType.description() == kPLUIImageViewController)) {
return true
}
return false
}
private func setPLCropOverlayBottomBar(view view: UIView) {
for myView in view.subviews {
if myView.isKindOfClass(NSClassFromString(kPLCropOverlayBottomBar)!) {
plCropOverlayBottomBar = myView
break
}
else {
savePLCropOverlayBottomBar(view: myView as UIView)
}
}
}
private func setCustomLayer(viewController viewController: UIViewController) {
let circlePath = getCirclePath()
let maskPath = getMaskPath(screenWidth: screenWidth, screenHeight: screenHeight, circlePath: circlePath)
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.CGPath
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.fillColor = UIColor.blackColor().colorWithAlphaComponent(0.8).CGColor
customLayer = maskLayer
viewController.view.layer.addSublayer(customLayer!)
viewController.view.addSubview(plCropOverlayBottomBar!) // put back overlayBottomBar once we set its parent to hidden (subview of PLCropOverlay)
}
}
Here is the solution which might help you to create crop overlay:-
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if ([navigationController.viewControllers count] == 3)
{
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
UIView *plCropOverlay = [[[viewController.view.subviews objectAtIndex:1]subviews] objectAtIndex:0];
plCropOverlay.hidden = YES;
int position = 0;
if (screenHeight == 568)
{
position = 124;
}
else
{
position = 80;
}
CAShapeLayer *circleLayer = [CAShapeLayer layer];
UIBezierPath *path2 = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, position, 320.0f, 320.0f)];
[path2 setUsesEvenOddFillRule:YES];
[circleLayer setPath:[path2 CGPath]];
[circleLayer setFillColor:[[UIColor clearColor] CGColor]];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 320, screenHeight-72) cornerRadius:0];
[path appendPath:path2];
[path setUsesEvenOddFillRule:YES];
CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor blackColor].CGColor;
fillLayer.opacity = 0.8;
[viewController.view.layer addSublayer:fillLayer];
UILabel *moveLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, 320, 50)];
[moveLabel setText:#"Move and Scale"];
[moveLabel setTextAlignment:NSTextAlignmentCenter];
[moveLabel setTextColor:[UIColor whiteColor]];
[viewController.view addSubview:moveLabel];
}
}
This should be a VERY easy problem but I'm having trouble getting the desired result. I have horizontally scrolling UIScrollView with a width of 320. The width of its content is 5000. I'm trying to center the content with:
// 'self' is the UIScrollView
CGFloat newContentOffsetX = (self.width/2) + (self.contentSize.width/2);
self.contentOffset = CGPointMake(newContentOffsetX, 0);
I don't understand why this is not centering the content. Can you see something wrong with the calculations?
I think you want:
CGFloat newContentOffsetX = (self.contentSize.width/2) - (self.bounds.size.width/2);
diagram:
|------------------------self.contentSize.width------------------------|
|-----self.width-----|
|-------- offset --------|
^ center
Use below code:
CGFloat newContentOffsetX = (scrollView.contentSize.width - scrollView.frame.size.width) / 2;
scrollView.contentOffset = CGPointMake(newContentOffsetX, 0);
Swift 4
Scroll to center of content of ScrollView
let centerOffsetX = (scrollView.contentSize.width - scrollView.frame.size.width) / 2
let centerOffsetY = (scrollView.contentSize.height - scrollView.frame.size.height) / 2
let centerPoint = CGPoint(x: centerOffsetX, y: centerOffsetY)
scrollView.setContentOffset(centerPoint, animated: true)
I created a simple UIScrollView extension with IBInspectable properties to center content horizontally and/or vertically. It works only when the scrollview content size is lower than the scrollview size.
Here's the Swift 5.2 code:
import UIKit
#IBDesignable class CenteredScrollView: UIScrollView {
#IBInspectable var verticallyCentered: Bool = false
#IBInspectable var horizontallyCentered: Bool = false
override var contentSize: CGSize {
didSet {
if contentSize.height <= bounds.height && verticallyCentered {
centerVertically()
}
if contentSize.width <= bounds.width && horizontallyCentered {
centerHorizontally()
}
}
}
private func centerVertically() {
let yContentOffset = (contentSize.height / 2) - (bounds.size.height / 2)
contentOffset = CGPoint(x: contentOffset.x, y: yContentOffset)
}
private func centerHorizontally() {
let xContentOffset = (contentSize.width / 2) - (bounds.size.width / 2)
contentOffset = CGPoint(x: xContentOffset, y: contentOffset.y)
}
}
Not the best solution but might work for you
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self centerScrollViewAnimated:NO];
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
[self centerScrollViewAnimated:YES];
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
*targetContentOffset = [self suggestedCenteredContentOffset];
}
- (CGPoint)suggestedCenteredContentOffset {
CGSize imageSize = self.pickedImage.size;
CGSize containerSize = self.zoomingScrollView.bounds.size;
CGPoint result = self.zoomingScrollView.contentOffset;
if ( self.zoomingScrollView.zoomScale * imageSize.width < containerSize.width )
result.x = MIN((self.zoomingScrollView.zoomScale * imageSize.width - containerSize.width)/2, 0);
if ( self.zoomingScrollView.zoomScale * imageSize.height < containerSize.height )
result.y = MIN((self.zoomingScrollView.zoomScale * imageSize.height - containerSize.height)/2, 0);
return result;
}
- (void)centerScrollViewAnimated:(BOOL)animated {
[self.zoomingScrollView setContentOffset:[self suggestedCenteredContentOffset] animated:animated];
}
Swift 5+
#IBDesignable class CenteredScrollView: UIScrollView {
#IBInspectable var isVerticallyCentered: Bool = false
#IBInspectable var isHorizontallyCentered: Bool = false
public func center(vertically: Bool, horizontally:Bool, animated: Bool) {
guard vertically || horizontally else {
return
}
var offset = contentOffset
if vertically && contentSize.height <= bounds.height {
offset.y = (contentSize.height / 2) - (bounds.size.height / 2)
}
if horizontally && contentSize.width <= bounds.width {
offset.x = (contentSize.width / 2) - (bounds.size.width / 2)
}
setContentOffset(offset, animated: animated)
}
override func layoutSubviews() {
super.layoutSubviews()
center(vertically: isVerticallyCentered, horizontally: isHorizontallyCentered, animated: false)
}
}