I have a camera view where users can either take a photo with their camera (which opens with the view) or click the photo library icon and select an image from their library. If they take the photo right then, I am able to bind capturedImage to UploadPostView, but I cannot figure out how to do the same if they choose a photo from their library. I'm creating something similar to Instagram stories or Snapchat where after you take/select the photo you are able to edit it (UploadPostView) before posting.
Binding the variable in the same way I do for capturedImage does not work. I am not super familiar but I figured it was because I was binding the actual ImagePicker class. but when I try to bind ImagePicker.image or ImagePicker.imageSelection... it also doesn't do anything. Thank you!!
CustomCameraView (where the user takes the photo or selects it from their library)
import SwiftUI
import PhotosUI
struct CustomCameraView: View {
let cameraService = CameraService()
@Environment(\.dismiss) private var dismiss
@StateObject var imagePicker = ImagePicker()
@Binding var capturedImage: UIImage?
var body: some View {
//if photo taken
if (imagePicker.image != nil) || (capturedImage != nil) {
UploadPostView(capturedImage: $capturedImage)
}
//if photo not taken yet
else {
ZStack (alignment: .topLeading) {
CameraView(cameraService: cameraService) { result in
switch result {
case .success(let photo):
if let data = photo.fileDataRepresentation() {
capturedImage = UIImage(data: data)
} else {
print("Error: no image data found")
}
case .failure(let err):
print(err.localizedDescription)
}
}
VStack (alignment: .leading) {
Button {
dismiss()
} label: {
Image("xmark")
.renderingMode(.template)
.resizable()
.frame(width: 28, height: 28)
.foregroundColor(.white)
}
.padding()
Spacer()
HStack {
PhotosPicker(selection: $imagePicker.imageSelection) {
Image("image-square")
.renderingMode(.template)
.resizable()
.frame(width: 32, height: 28)
.foregroundColor(.white)
}
Spacer()
Button {
cameraService.capturePhoto()
} label: {
Image(systemName: "circle")
.font(.system(size: 72))
.foregroundColor(.white)
}
Spacer()
Rectangle()
.foregroundColor(.clear)
.frame(width: 32, height: 28)
}
.padding()
}
}
.cornerRadius(6)
.background(.black)
}
}
}
ImagePicker
import SwiftUI
import PhotosUI
@MainActor
class ImagePicker: ObservableObject {
@Published var image: Image?
@Published var uiImage: UIImage?
@Published var imageSelection: PhotosPickerItem? {
didSet {
if let imageSelection {
Task {
try await loadTransferable(from: imageSelection)
}
}
}
}
func loadTransferable(from imageSelection: PhotosPickerItem?) async throws {
do {
if let data = try await imageSelection?.loadTransferable(type: Data.self) {
if let uiImage = UIImage(data: data) {
self.uiImage = uiImage
self.image = Image(uiImage: uiImage)
}
}
} catch {
print(error.localizedDescription)
image = nil
}
}
}
UploadPostView (where users can edit their photo before uploading)
import SwiftUI
import Kingfisher
import PhotosUI
struct UploadPostView: View {
@Environment(\.dismiss) private var dismiss
@ObservedObject var viewModel = UploadPostViewModel()
@State var caption = ""
@State var rating = 0
@StateObject var imagePicker = ImagePicker()
@Binding var capturedImage: UIImage?
var body: some View {
VStack {
if let image = imagePicker.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
.cornerRadius(6)
.clipped()
}
else {
if let image = capturedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
.cornerRadius(6)
.clipped()
}
}
HStack {
Spacer()
Button {
if let uiimage = imagePicker.uiImage {
viewModel.uploadPost(caption: caption, image: uiimage, rating: rating)
viewModel.loading = true
} else if let uiimage = capturedImage {
viewModel.uploadPost(caption: caption, image: uiimage, rating: rating)
viewModel.loading = true
}
} label: {
if viewModel.loading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.frame(width: 24, height: 24)
.padding()
.background(Color.accentColor)
.foregroundColor(.white)
.clipShape(Circle())
} else {
Image("send-fill")
.renderingMode(.template)
.resizable()
.frame(width: 24, height: 24)
.padding()
.background(Color.accentColor)
.foregroundColor(.white)
.clipShape(Circle())
}
}
}.padding(8)
}
.background(.black)
.onReceive(viewModel.$didUploadPost) { success in
if success {
dismiss()
}
}
}
}
There are 3 issues with your code.
ImagePickerand having multiple variables for an image (capturedImage,uiImageandimage)Every time you call
ImagePicker()you create a different instance and one does not know about the other.There are many ways to solve this but I prefer self contained reusable modules when possible.
You can achieve this by putting all the code for the Picker in its own
View.With the above approach you eliminate the need for
ImagePickerand start working directly withcapturedImage.Now this leads to the third issue. You code is small now and all fresh in your mind but if you have to revisit this code in in a few months you will likely encounter areas that are fragile. Such as conditionals that fall through or in other words you overlook
else. The first one was in theloadTransferablefunction and the next is inUploadPostView.Assuming that your plan is to never show
UploadPostViewunless there is acapturedImageyou can improve yournilcheck to eliminate the optional in thatViewThis will clean up the
Viewsignificantly.