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.
ImagePicker
and having multiple variables for an image (capturedImage
, uiImage
and image
)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.
PhotosPicker { result in
switch result {
case .success(let image):
capturedImage = image
case .failure(let error):
print(error)//Provides a better description of an error.
capturedImage = nil
}
}
You can achieve this by putting all the code for the Picker in its own View
.
import PhotosUI
struct PhotosPicker: View{
@State private var imageSelection: PhotosPickerItem?
//
let action: (Result<UIImage, Error>) async -> Void
var body: some View{
PhotosUI.PhotosPicker(selection: $imageSelection, matching: .images) {
Image(systemName: "photo")
.renderingMode(.template)
.resizable()
.frame(width: 32, height: 28)
.foregroundColor(.white)
}.task(id: imageSelection) {//Will trigger when there is a change to imageSelection
if let _ = imageSelection{
do{
let image = try await loadTransferable()
await action(.success(image))
}catch{
await action(.failure(error))
}
}
}
}
func loadTransferable() async throws -> UIImage {
guard let data = try await imageSelection?.loadTransferable(type: Data.self) else{
throw PickerError.unableGetData
}
//Make sure you don't overlook `else` when dealing with conditionals. The user should always be informed
guard let uiImage = UIImage(data: data) else {
throw PickerError.unableToCreateUIImage
}
return uiImage
}
enum PickerError: LocalizedError{
case unableGetData
case unableToCreateUIImage
}
}
With the above approach you eliminate the need for ImagePicker
and start working directly with capturedImage
.
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 the loadTransferable
function and the next is in UploadPostView
.
Assuming that your plan is to never show UploadPostView
unless there is a capturedImage
you can improve your nil
check to eliminate the optional in that View
struct CustomCameraView: View {
@Environment(\.dismiss) private var dismiss
@Binding var capturedImage: UIImage?
var body: some View {
//Unwrap Binding, to get rid of the optional below
if let b = Binding($capturedImage) {
UploadPostView(capturedImage: b)
}else {
ZStack (alignment: .topLeading) {
VStack (alignment: .leading) {
Button {
dismiss()
} label: {
Image(systemName: "xmark")
.renderingMode(.template)
.resizable()
.frame(width: 28, height: 28)
.foregroundColor(.white)
}
.padding()
Spacer()
HStack {
PhotosPicker { result in
switch result {
case .success(let image):
capturedImage = image
case .failure(let error):
print(error)
capturedImage = nil
}
}
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)
}
}
}
This will clean up the View
significantly.
struct UploadPostView: View {
@Environment(\.dismiss) private var dismiss
@Binding var capturedImage: UIImage //Optional removed
var body: some View {
VStack {
Image(uiImage: capturedImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
.cornerRadius(6)
.clipped()
}
.background(.black)
}
}