Add photo picker to conversation view

This commit is contained in:
Benoit Martins 2024-05-21 17:25:02 +02:00
parent 0682489645
commit 472bf46938
4 changed files with 342 additions and 42 deletions

View file

@ -161,7 +161,7 @@ struct ChatBubbleView: View {
func messageAttachments() -> some View {
if message.attachments.count == 1 {
if message.attachments.first!.type == .image || message.attachments.first!.type == .gif || message.attachments.first!.type == .video {
let result = imageDimensions(url: message.attachments.first!.full.absoluteString)
let result = imageDimensions(url: message.attachments.first!.thumbnail.absoluteString)
ZStack {
Rectangle()
.fill(Color(.white))
@ -186,31 +186,59 @@ struct ChatBubbleView: View {
}
if message.attachments.first!.type == .image || message.attachments.first!.type == .video {
AsyncImage(url: message.attachments.first!.full) { image in
ZStack {
image
.resizable()
.interpolation(.medium)
.aspectRatio(contentMode: .fill)
if message.attachments.first!.type == .video {
Image("play-fill")
.renderingMode(.template)
if #available(iOS 16.0, *) {
AsyncImage(url: message.attachments.first!.thumbnail) { image in
ZStack {
image
.resizable()
.foregroundStyle(.white)
.frame(width: 40, height: 40, alignment: .leading)
.interpolation(.medium)
.aspectRatio(contentMode: .fill)
if message.attachments.first!.type == .video {
Image("play-fill")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 40, height: 40, alignment: .leading)
}
}
} placeholder: {
ProgressView()
}
.layoutPriority(-1)
} else {
AsyncImage(url: message.attachments.first!.thumbnail) { image in
ZStack {
image
.resizable()
.interpolation(.medium)
.aspectRatio(contentMode: .fill)
if message.attachments.first!.type == .video {
Image("play-fill")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 40, height: 40, alignment: .leading)
}
}
} placeholder: {
ProgressView()
}
} placeholder: {
ProgressView()
}
.id(UUID())
.layoutPriority(-1)
} else if message.attachments.first!.type == .gif {
GifImageView(message.attachments.first!.full)
.id(UUID())
.layoutPriority(-1)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
} else if message.attachments.first!.type == .gif {
if #available(iOS 16.0, *) {
GifImageView(message.attachments.first!.thumbnail)
.layoutPriority(-1)
.clipShape(RoundedRectangle(cornerRadius: 4))
} else {
GifImageView(message.attachments.first!.thumbnail)
.id(UUID())
.layoutPriority(-1)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 4))
@ -227,29 +255,51 @@ struct ChatBubbleView: View {
.fill(Color(.white))
.frame(width: 120, height: 120)
AsyncImage(url: attachment.full) { image in
ZStack {
image
.resizable()
.interpolation(.medium)
.aspectRatio(contentMode: .fill)
if attachment.type == .video {
Image("play-fill")
.renderingMode(.template)
if #available(iOS 16.0, *) {
AsyncImage(url: attachment.thumbnail) { image in
ZStack {
image
.resizable()
.foregroundStyle(.white)
.frame(width: 40, height: 40, alignment: .leading)
.interpolation(.medium)
.aspectRatio(contentMode: .fill)
if attachment.type == .video {
Image("play-fill")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 40, height: 40, alignment: .leading)
}
}
} placeholder: {
ProgressView()
}
} placeholder: {
ProgressView()
.layoutPriority(-1)
} else {
AsyncImage(url: attachment.thumbnail) { image in
ZStack {
image
.resizable()
.interpolation(.medium)
.aspectRatio(contentMode: .fill)
if attachment.type == .video {
Image("play-fill")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 40, height: 40, alignment: .leading)
}
}
} placeholder: {
ProgressView()
}
.id(UUID())
.layoutPriority(-1)
}
.id(UUID())
.layoutPriority(-1)
}
.clipShape(RoundedRectangle(cornerRadius: 4))
.clipped()
.contentShape(Rectangle())
}
}
.frame(

View file

@ -44,6 +44,13 @@ struct ConversationFragment: View {
@State private var displayFloatingButton = false
@State private var isShowPhotoLibrary = false
@State private var isShowCamera = false
@State private var mediasToSend: [Attachment] = []
@State private var mediasIsLoading = false
@State private var maxMediaCount = 12
var body: some View {
NavigationView {
GeometryReader { geometry in
@ -313,6 +320,93 @@ struct ConversationFragment: View {
}
}
if !mediasToSend.isEmpty || mediasIsLoading {
ZStack(alignment: .top) {
HStack {
if mediasIsLoading {
HStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 120)
}
LazyVGrid(columns: [
GridItem(.adaptive(minimum: 100), spacing: 1)
], spacing: 3) {
ForEach(mediasToSend, id: \.id) { attachment in
ZStack {
Rectangle()
.fill(Color(.white))
.frame(width: 100, height: 100)
AsyncImage(url: attachment.thumbnail) { image in
ZStack {
image
.resizable()
.interpolation(.medium)
.aspectRatio(contentMode: .fill)
if attachment.type == .video {
Image("play-fill")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 40, height: 40, alignment: .leading)
}
}
} placeholder: {
ProgressView()
}
.layoutPriority(-1)
.onTapGesture {
if mediasToSend.count == 1 {
withAnimation {
mediasToSend = []
}
} else {
guard let index = self.mediasToSend.firstIndex(of: attachment) else { return }
self.mediasToSend.remove(at: index)
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 4))
.contentShape(Rectangle())
}
}
.frame(
width: geometry.size.width > 0 && CGFloat(102 * mediasToSend.count) > geometry.size.width - 20
? 102 * floor(CGFloat(geometry.size.width - 20) / 102)
: CGFloat(102 * mediasToSend.count)
)
}
.frame(maxWidth: .infinity)
.padding(.all, mediasToSend.isEmpty ? 0 : 10)
.background(Color.gray100)
if !mediasIsLoading {
HStack {
Spacer()
Button(action: {
withAnimation {
mediasToSend = []
}
}, label: {
Image("x")
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
.padding(.all, 10)
})
}
}
}
.transition(.move(edge: .bottom))
}
HStack(spacing: 0) {
Button {
} label: {
@ -327,26 +421,31 @@ struct ConversationFragment: View {
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
Button {
self.isShowPhotoLibrary = true
self.mediasIsLoading = true
} label: {
Image("paperclip")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.foregroundStyle(maxMediaCount <= mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading)
.padding(.all, isMessageTextFocused ? 0 : 6)
.padding(.top, 4)
.disabled(maxMediaCount <= mediasToSend.count || mediasIsLoading)
}
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
Button {
self.isShowCamera = true
} label: {
Image("camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.foregroundStyle(maxMediaCount <= mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading)
.padding(.all, isMessageTextFocused ? 0 : 6)
.padding(.top, 4)
.disabled(maxMediaCount <= mediasToSend.count || mediasIsLoading)
}
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
@ -437,6 +536,28 @@ struct ConversationFragment: View {
.onDisappear {
conversationViewModel.removeConversationDelegate()
}
.sheet(isPresented: $isShowPhotoLibrary) {
PhotoPicker(filter: nil, limit: maxMediaCount - mediasToSend.count) { results in
PhotoPicker.convertToAttachmentArray(fromResults: results) { mediasOrNil, errorOrNil in
if let error = errorOrNil {
print(error)
}
if let medias = mediasOrNil {
mediasToSend.append(contentsOf: medias)
}
self.mediasIsLoading = false
}
}
.edgesIgnoringSafeArea(.all)
}
/*
.fullScreenCover(isPresented: $isShowCamera) {
ImagePicker(selectedImage: self.$image, sourceType: .camera)
.edgesIgnoringSafeArea(.all)
}
*/
}
}
.navigationViewStyle(.stack)
@ -456,6 +577,48 @@ extension UIApplication {
}
}
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage
@Environment(\.presentationMode) private var presentationMode
var sourceType: UIImagePickerController.SourceType = .photoLibrary
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
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 makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
/*
#Preview {
ConversationFragment(conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel(), sections: [MessagesSection], ids: [""])

View file

@ -32,4 +32,6 @@ extension View {
self
}
}
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
}

View file

@ -23,14 +23,16 @@ import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
let filter: PHPickerFilter
let filter: PHPickerFilter?
var limit: Int = 0
let onComplete: ([PHPickerResult]) -> Void
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = filter
if filter != nil {
configuration.filter = filter
}
configuration.selectionLimit = limit
let controller = PHPickerViewController(configuration: configuration)
@ -63,6 +65,89 @@ struct PhotoPicker: UIViewControllerRepresentable {
}
}
static func convertToAttachmentArray(fromResults results: [PHPickerResult], onComplete: @escaping ([Attachment]?, Error?) -> Void) {
var medias = [Attachment]()
let dispatchGroup = DispatchGroup()
for result in results {
dispatchGroup.enter()
let itemProvider = result.itemProvider
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { urlFile, error in
if urlFile != nil {
do {
let dataResult = try Data(contentsOf: urlFile!)
let urlImage = self.saveMedia(name: urlFile!.lastPathComponent, data: dataResult, type: .image)
if urlImage != nil {
let attachment = Attachment(id: UUID().uuidString, url: urlImage!, type: .image)
medias.append(attachment)
}
} catch {
}
}
dispatchGroup.leave()
}
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { urlFile, error in
if urlFile != nil {
do {
let dataResult = try Data(contentsOf: urlFile!)
let urlImage = self.saveMedia(name: urlFile!.lastPathComponent, data: dataResult, type: .video)
let urlThumbnail = getURLThumbnail(name: urlFile!.lastPathComponent)
if urlImage != nil {
let attachment = Attachment(id: UUID().uuidString, thumbnail: urlThumbnail, full: urlImage!, type: .video)
medias.append(attachment)
}
} catch {
}
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
onComplete(medias, nil)
}
}
static func saveMedia(name: String, data: Data, type: AttachmentType) -> URL? {
do {
let path = FileManager.default.temporaryDirectory.appendingPathComponent((name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""))
let decodedData: () = try data.write(to: path)
if type == .video {
let asset = AVURLAsset(url: path, options: nil)
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage)
guard let data = thumbnail.jpegData(compressionQuality: 1) ?? thumbnail.pngData() else {
return nil
}
let urlName = FileManager.default.temporaryDirectory.appendingPathComponent("preview_" + (name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "") + ".png")
let decodedData: () = try data.write(to: urlName)
}
return path
} catch let error {
print("*** Error generating thumbnail: \(error.localizedDescription)")
return nil
}
}
static func getURLThumbnail(name: String) -> URL {
return FileManager.default.temporaryDirectory.appendingPathComponent("preview_" + (name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "") + ".png")
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {