I have a .onTapGesture modifier that when tapped presents an ImagePicker before immediately dismissing it.
#State private var updateInfo = false
var body: some View {
HStack {
placeholder.image
.resizable()
.aspectRatio(contentMode: .fill)
.font(.system(size: 16, weight: .medium))
.frame(width: 100, height: 100)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.1), radius: 1, x: 0, y: 1)
.onTapGesture {
updateInfo.showImagePicker = true
}
.sheet(isPresented: $updateInfo.showImagePicker) {
ImagePicker(showImagePicker: $updateInfo.showImagePicker, pickedImage: $updateInfo.image, imageData: $updateInfo.imageData)
}
}
}
Here's my ImagePicker
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
#Binding var showImagePicker: Bool
#Binding var pickedImage: Image
#Binding var imageData: Data
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
return
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parentImagePicker: ImagePicker
init(_ imagePicker: ImagePicker) {
self.parentImagePicker = imagePicker
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
parentImagePicker.pickedImage = Image(uiImage: uiImage)
if let mediaImage = uiImage.jpegData(compressionQuality: 0.5) {
parentImagePicker.imageData = mediaImage
}
parentImagePicker.showImagePicker = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parentImagePicker.showImagePicker = false
}
}
}
I keep getting this error in the console: Unbalanced calls to begin/end appearance transitions for <_UIImagePickerPlaceholderViewController: 0x...>
Not sure where I'm transitioning incorrectly. I have a tab bar view with three tabs. The final tab has a navigation bar item that pushes the detail updateInfo view.
I tap the placeholder image and system presents the image picker controller (kind of) before immediately dismissing. I tap it again and the image picker controller presents.
Thoughts on why it dismisses the first time?
A solution that is not exactly a solution
I was trying to call the ImagePickerController on a view and kept getting the unbalanced views error. So I created a different view entirely for picking a new image. The error has not been addressed, but the issue for me is solved.
Related
I'm using a UIImagePickerController to select an image from the user's photo library. All works fine except when the user taps on the field with the magnifying glass icon to search the library. Sometimes it dismisses ImagePicker, other times it does nothing and occasionally it works as expected... any ideas?
Code to display the sheet:
Button(action: {}) {
ZStack {
Image(systemName: "photo")
.foregroundColor(darkMode ? .white : .black)
}
}
.frame(width: 44, height: 44)
.onTapGesture {
showPhotoLibrary.toggle()
}
.sheet(isPresented: $showPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
The ImagePicker:
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
internal func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
I've seen many struggle with this, it's all over the Apple dev forums so I thought I would have a go at solving it. From my own testing I believe the reason for the problems is UIImagePickerController is designed to be presented using the present method of a parent View Controller, not used as the root view controller of a SwiftUI presented sheet, as Apple (in their sample code) and everyone else seem to be doing. Below is my incomplete test code, which shows a working image picker where the search works and it can also be dragged down to dismiss, so I think this might be the correct solution.
The idea is we use UIViewControllerRepresentable to present our own custom view controller that presents or dismisses the image picker view controller when the present boolean changes.
struct ImagePickerTest: View {
#State var show = false
#State var image1: UIImage?
var body: some View {
VStack {
Button("Hello, World!") {
show.toggle()
}
Text("image: \(image1?.description ?? "" )")
}
.imagePicking(isPresented: $show, selectedImage: $image1)
}
}
extension View {
// this could be refactored into a `ViewModifier`
func imagePicking(isPresented: Binding<Bool>, selectedImage: Binding<UIImage?>) -> some View {
ZStack {
ImagePicking(isPresented: isPresented, selectedImage: selectedImage)
self
}
}
}
class MyViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate{
var dismissPicker: (() -> Void)?
var selectImage: ((UIImage) -> Void)?
func showPickerIfNecessary() {
if self.presentedViewController != nil {
return
}
let imagePickerController = UIImagePickerController()
imagePickerController.sourceType = .photoLibrary
imagePickerController.delegate = self
present(imagePickerController, animated: true)
}
func hidePickerIfNecessary() {
if let vc = self.presentedViewController {
vc.dismiss(animated: true)
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
print("imagePickerControllerDidCancel")
dismissPicker?()
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
print("didFinishPickingMediaWithInfo")
if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage {
selectImage?(image)
dismissPicker?()
}
}
}
struct ImagePicking: UIViewControllerRepresentable {
#Binding var isPresented: Bool
#Binding var selectedImage: UIImage?
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
uiViewController.dismissPicker = {
isPresented = false
}
uiViewController.selectImage = { image in
selectedImage = image
}
if isPresented {
uiViewController.showPickerIfNecessary()
} else {
uiViewController.hidePickerIfNecessary()
}
}
func makeUIViewController(context: Context) -> some MyViewController {
MyViewController()
}
}
Before I started, I verified the OP's code was failing when typing a search, it crashed with this error:
2022-08-31 22:39:17.808899+0100 Test[66967:5425868] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] Error Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}
I'm reposting my question of yesterday and now adding a clean code example to demonstrate the problem
I have a MyCustomMapView, embedding a MKMApView and it starts at a fixed location. I have a function called gotoCoordinate, which accepts a coordinate and then navigates the mapview's center to that coordinate.
In the sample code that can be simulated by clicking on the red button labelleing "Click here to change map position".
This all works great. Until....
in the app I'm working on I also need to have a user location so I have a LocationViewModel handling the request. Once you have given request to access your location, click the button no longer moves the center of the map to that new coordinate.
Once you comment the #StateObject var locationViewModel = LocationViewModel() it is working again.
So it seems that once you are using a location manager with a delegate the map no longer moves when changing it's region
Is this a bug or am I doing something wrong?
import SwiftUI
struct ContentView: View {
#StateObject var locationViewModel = LocationViewModel()
var body: some View {
switch locationViewModel.authorizationStatus {
case .notDetermined:
AnyView(RequestLocationView())
.environmentObject(locationViewModel)
case .restricted:
ErrorView(errorText: "Location use is restricted.")
case .denied:
ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
default:
EmptyView()
}
GeometryReader { geometry in
DisplayMapView(size:geometry.size)
}
}
}
import SwiftUI
import CoreLocation
import MapKit
struct MyCustomMapView: UIViewRepresentable {
var map = MKMapView() // << constructor contract !!
let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:31,longitude: -86 )
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
map.showsUserLocation = true
map.showsCompass = true
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
map.setRegion(region, animated: true)
return map
}
func gotoCoordinate(_ newCoordinate: CLLocationCoordinate2D ){
let region = MKCoordinateRegion(center: newCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
map.setRegion(region, animated: true)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MyCustomMapView.Coordinator {
return MyCustomMapView.Coordinator(parent1: self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent:MyCustomMapView
init(parent1:MyCustomMapView){
parent = parent1
}
}//class Coordinator
}
import SwiftUI
import CoreLocation
import MapKit
struct DisplayMapView: View {
#Environment(\.presentationMode) var presentationMode
var size: CGSize
var startCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
var map = MyCustomMapView()
var body: some View {
ZStack(alignment:.top){
map
VStack(alignment:.leading){
HStack {
HStack {
Text("Click here to change map position")
.onTapGesture(){
map.gotoCoordinate(startCoordinate)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.black)
.background(Color(.red))
.cornerRadius(10.0)
}
}.padding(.top,50).padding(.leading,20).padding(.trailing,20)
}.ignoresSafeArea()
}
}
import Foundation
import SwiftUI
import CoreLocation
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var authorizationStatus: CLAuthorizationStatus
#Published var lastSeenLocation: CLLocation?
#Published var currentPlacemark: CLPlacemark?
private let locationManager: CLLocationManager
static let shared = LocationViewModel()
override init() {
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 0.4
locationManager.startUpdatingLocation()
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastSeenLocation = locations.first
}
}
struct RequestLocationView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
var body: some View {
VStack(spacing:50) {
Image(systemName: "location.circle")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
.foregroundColor(Color.init(red: 0.258, green: 0.442, blue: 0.254))
Button(action: {
locationViewModel.requestPermission()
}, label: {
Label(LocalizedStringKey("allowLocationAccess"), systemImage: "location")
})
.padding(10)
.foregroundColor(.white)
.background(.green)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text("We need your permission to give you the best experience.")
.foregroundColor(.gray)
.font(.caption)
}
}
}
struct ErrorView: View {
var errorText: String
var body: some View {
VStack {
Image(systemName: "xmark.octagon")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
Text(errorText)
}
.padding()
.foregroundColor(.white)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Declare your coordinates as a stateful variable, either as #State or as #Published within an observable object:
struct DisplayMapView: View {
#State var coordinates = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
Then pass the coordinates in as an argument to your view - no need to store your view as a variable:
ZStack(alignment: .top) {
MyMapView(coordinates: coordinates)
VStack(alignment: .leading) {
// etc.
Then you’ll need to do some rejigging in your UIViewRepresentable. You mustn't retain map as a separate instance outside makeUIView and updateUIView - SwiftUI structs can be recreated at will, so that would release your MKMapView instance and create a new one. Instead, the object returned by makeUIView is retained for you by the system. You do need to declare a variable that will accept the coordinates argument above, and then respond to any changes in it in updateUIView.
struct MyMapView: UIViewRepresentable {
var coordinates: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
// etc.
return map
}
func updateUIView(_ uiView: MKMapView, context: Coordinator) {
let region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
uiView.setRegion(region, animated: true)
}
}
Now, when the user taps, instead of calling a function inside your view, you update the DisplayMapView’s coordinates variable and the UIViewRepresentable’s update logic should redraw the map in the correct position.
Using Swift5.3.2, iOS14.4.1, Xcode12.4,
I am trying to use the .simultaneousGesture modifier in SwiftUI.
As far as I understood, this modifier should make sure that gestures (such as tap, longpress, magnification etc) should be able to co-exist within a View.
In my example I am using a ZoomableScrollView. And it works fine as long as I do not use the simultaneousGesture.
But as soon as I use the extra simultaneousGesture, the ZoomableScrollView is no longer "zoomable" (i.e. none of its gestures work anymore).
What can I do to make the zoom still work AND get an extra dragGesture ?
import SwiftUI
struct MediaTabView: View {
#GestureState private var dragOffset: CGFloat = -100
var body: some View {
ZoomableScrollView {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.simultaneousGesture(
DragGesture()
.updating($dragOffset) { (value, gestureState, transaction) in
let delta = value.location.x - value.startLocation.x
if delta > 10 { // << some appropriate horizontal threshold here
gestureState = delta
print(delta)
}
}
.onEnded {
if $0.translation.width > 100 {
// Go to the previous slide
print("on ended")
}
}
)
}
}
The code for the ZoomableScrollView is here :
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
Using Swift 2.0 I am hoping to find a way to capture the resized image after the user has selected how they want to see it in the frame from the scroll view (ZoomScrollView).
I know there are complex examples out there from Swift but was hoping to find a simpler way to capture this in Swift 2.0. In all my searching I've heard references to using ZStack and some masks or overlays but can't find a simple good example.
I am hoping someone can update my example with the ZStack, masks, etc and how to extract the image for saving or provide a better example.
import SwiftUI
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#State var isAccepted: Bool = false
#State var isShowingImagePicker = false
#State var isShowingActionPicker = false
#State var sourceType:UIImagePickerController.SourceType = .camera
#State var image:UIImage?
var body: some View {
HStack {
Color(UIColor.systemYellow).frame(width: 8)
VStack(alignment: .leading) {
HStack {
Spacer()
VStack {
if image != nil {
ZoomScrollView {
Image(uiImage: image!)
.resizable()
.scaledToFit()
}
.frame(width: 300, height: 300, alignment: .center)
.clipShape(Circle())
} else {
Image(systemName: "person.crop.circle")
.resizable()
.font(.system(size: 32, weight: .light))
.frame(width: 300, height: 300, alignment: .center)
.cornerRadius(180)
.foregroundColor(Color(.systemGray))
.clipShape(Circle())
}
}
Spacer()
}
Spacer()
HStack {
Button(action: {
self.isShowingActionPicker = true
}, label: {
Text("Select Image")
.foregroundColor(.blue)
})
.frame(width: 130)
.actionSheet(isPresented: $isShowingActionPicker, content: {
ActionSheet(title: Text("Select a profile avatar picture"), message: nil, buttons: [
.default(Text("Camera"), action: {
self.isShowingImagePicker = true
self.sourceType = .camera
}),
.default(Text("Photo Library"), action: {
self.isShowingImagePicker = true
self.sourceType = .photoLibrary
}),
.cancel()
])
})
.sheet(isPresented: $isShowingImagePicker) {
imagePicker(image: $image, isShowingImagePicker: $isShowingImagePicker ,sourceType: self.sourceType)
}
Spacer()
// Save button
Button(action: {
// Save Image here... print for now just see if file dimensions are the right size
print("saved: ", image!)
self.presentationMode.wrappedValue.dismiss()
}
) {
HStack {
Text("Save").foregroundColor(isAccepted ? .gray : .blue)
}
}
.frame(width: 102)
.padding(.top)
.padding(.bottom)
//.buttonStyle(RoundedCorners())
.disabled(isAccepted) // Disable if if already isAccepted is true
}
}
Spacer()
Color(UIColor.systemYellow).frame(width: 8)
}
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top)
.background(Color(UIColor.systemYellow))
}
}
struct ZoomScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
struct imagePicker:UIViewControllerRepresentable {
#Binding var image: UIImage?
#Binding var isShowingImagePicker: Bool
typealias UIViewControllerType = UIImagePickerController
typealias Coordinator = imagePickerCoordinator
var sourceType:UIImagePickerController.SourceType = .camera
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> imagePickerCoordinator {
return imagePickerCoordinator(image: $image, isShowingImagePicker: $isShowingImagePicker)
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
}
class imagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var image: UIImage?
#Binding var isShowingImagePicker: Bool
init(image:Binding<UIImage?>, isShowingImagePicker: Binding<Bool>) {
_image = image
_isShowingImagePicker = isShowingImagePicker
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiimage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
image = uiimage
isShowingImagePicker = false
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShowingImagePicker = false
}
}
Just want to return the image that's zoomed in the circle. The image can be square (re: the 300x300 frame), that's fine just need the zoomed image not whole screen or the original image.
the following changes were successful based the comments:
Add the following State variables:
#State private var rect: CGRect = .zero
#State private var uiimage: UIImage? = nil // resized image
Added "RectGetter" to the picked image frame after image selected selected
if image != nil {
ZoomScrollView {
Image(uiImage: image!)
.resizable()
.scaledToFit()
}
.frame(width: 300, height: 300, alignment: .center)
.clipShape(Circle())
.background(RectGetter(rect: $rect))
Here is the struct and extension I added
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
struct RectGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { proxy in
self.createView(proxy: proxy)
}
}
func createView(proxy: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = proxy.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}
Last I set the image to save
self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect)
This assumes the root controller. However, in my production app I had to point to self
self.uiimage = UIApplication.shared.windows[0].self.asImage(rect: self.rect)
Then I was able to save that image.
A couple of notes. The image returned is the rectangle which is fine. However due to the way the image is captured the rest of the rectangle outside the cropShape of a circle has the background color. In this case yellow at the for corners outside the circle. There is probably a way to have some sort of ZOrder mask that overlays the image for display when you are resizing the image but then this accesses the right layer and saves the full rectangle picture. If anyone wants to suggest further that would be a cleaner solution but this works assuming you will always display the picture in the same crop shape it was saved in.
I present a UIImagePickerController within my application by presenting it with logic inside of a sheet modifier. In short, the following three types handle displaying and dismissing a instance of UIImagePickerController inside of a UIViewControllerRepresentable type, which works as expected:
struct DetailsView: View {
enum Sheet: Hashable, Identifiable {
case takePhoto
var id: Int { hashValue }
}
#State private var activeSheet: Sheet?
var body: some View {
Text("Hello, World!")
.sheet(item: $activeSheet) { (sheet) in self.view(for: sheet) }
}
private func view(for sheet: Sheet) -> some View {
switch sheet {
case .takePhoto: return PhotoSelectionView(showImagePicker: .init(get: { sheet == .takePhoto }, set: { (show) in self.activeSheet = show ? .takePhoto : nil }), image: $selectedImage, photoSource: .camera).edgesIgnoringSafeArea(.all)
}
}
}
struct ImagePicker: UIViewControllerRepresentable {
#Binding var isShown: Bool
#Binding var image: Image?
let photoSource: PhotoSource
func makeCoordinator() -> ImagePickerCoordinator {
return ImagePickerCoordinator(isShown: $isShown, selectedImage: $image)
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
if photoSource == .camera, UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePicker.sourceType = .camera
imagePicker.cameraCaptureMode = .photo
}
imagePicker.delegate = context.coordinator
return imagePicker
}
}
class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding private var isShown: Bool
#Binding private var selectedImage: Image?
init(isShown: Binding<Bool>, selectedImage: Binding<Image?>) {
_isShown = isShown
_selectedImage = selectedImage
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
// handle photo selection
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss()
}
}
The issue that I am having is that the camera view is presented modally, which doesn't cover the entire screen. This causes the UIImagePickerController to appear to have broken layouts at times when the camera is the source, as if the camera was not made to be presented in this way. Setting imagePicker.modalPresentationStyle = .fullScreen does not result in a full-screen presentation.
How can I display the camera in a full-screen layout so that it does not appear in the card-like presentation style?
combining #LaX and #GrandSteph's answers, I have:
.fullScreenCover(isPresented: $activeSheet, content: {
ImagePicker(image: $inputImage, sourceType: .camera)
.edgesIgnoringSafeArea(.all)
})
With iOS 14 Apple has added the fullScreenCover(ispresented:ondismiss:content:) and fullScreenCover(item:ondismiss:content:) methods which do exactly what you are requesting.
From your example:
var body: some View {
Text("Hello, World!")
.fullScreenCover(item: $activeSheet) { (sheet) in self.view(for: sheet) }
}
Or, if you simply have a view you want to show:
#State var customViewIsShown = false
// ...
var body: some View {
Text("Hello, World!")
.fullScreenCover(isPresented: $customViewIsShown) {
YourCustomView(isShown: $customViewIsShown)
}
}
Ok, take this answer with a large grain of salt. I'll probably be downvoted to hell because of that and all my coder kids with me ... But so should Apple for not providing an easy way to present full screen a UIImagePickerController in SwiftUI.
The very bad the trick is to create your picker in the rootView and have it ignore safe area. Then you pass all necessary parameters as bindings to the view needing the imagePicker
struct mainView: View {
#State private var imagePicked = UIImage()
#State private var showImagePicker = false
#State private var pickerSource = UIImagePickerController.SourceType.camera
var body: some View {
ZStack {
AvatarView(showImagePicker: self.$showImagePicker, pickerSource: self.$pickerSource , imagePicked: self.$imagePicked)
if self.showImagePicker {
ImagePickerView(isPresented: self.$showImagePicker, selectedImage: self.$imagePicked, source: .camera).edgesIgnoringSafeArea(.all)
}
}
}
}
Of course, your Avatar view will have all the necessary code to update these bindings. Something like this
HStack () {
Button(action: {
self.showActionSheet.toggle()
}) {
MyImageView(image: self.imagePicked)
}
.actionSheet(isPresented: $showActionSheet, content: {
ActionSheet(title: Text("Picture source"), buttons: [
.default(Text("Camera"), action: {
self.pickerSource = .camera
self.showImagePicker.toggle()
}),
.default(Text("Photo Library"), action: {
self.pickerSource = .photoLibrary
self.showImagePicker.toggle()
}),
.destructive(Text("Cancel"))
])
})
}
Honestly I was hesitant to provide this solution, this code makes my eyes bleed but figured someone might have a better idea when reading this and provide a real clean SwiftUI way of doing it.
The good thing about this solution is that it manages well the changes of orientation and let's face it, the UIImagePickerController doesn't fit properly a sheet. It's fine for photo library but not camera.
If you wrap your ImagePicker in a container with black background, that will result in what I think you want to get:
.fullScreenCover(isPresented: $showPhotoPicker) {
ImagePicker(sourceType: .camera, selectedImage: self.$image)
// frame modifier adds a container with given size preferences
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black)
}
where