How to share a subview in a ShareSheet in SwiftUI - swift

I am trying to share an image of a view through the Share Sheet. This is what i am doing.
One defines an extension to UIView and View where one utilizes the UIGraphicsImageRenderer to output an UIImage and modifies the .asImage to output the picture as an UIImage.
It seems to me the simplest approach to making it work. But the image is not showing in the ShareSheet.
import SwiftUI
struct SnapshottingAPI: View {
#State private var isShareSheetShowing = false
let imageSize: CGSize = CGSize(width: 1000, height: 1000)
var body: some View {
VStack {
ZStack {
Color.blue
Circle()
.fill(Color.red)
}
.frame(width: 300, height: 300)
Text("text")
Button(action: shareButton){
Image(systemName: "square.and.arrow.up")
.frame(width: 35, height: 35)
}
}
}
func shareButton() {
isShareSheetShowing.toggle()
let highresImage = self.asImage(size: imageSize)
let outputimage = Image(uiImage: highresImage)
let items: [Any] = [outputimage]
let av = UIActivityViewController(activityItems: items, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController!.present(av, animated: true, completion: nil)
}
}
extension UIView {
func asImage() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: self.layer.frame.size, format: format).image { context in
self.drawHierarchy(in: self.layer.bounds, afterScreenUpdates: true)
}
}
}
extension View {
func asImage(size: CGSize) -> UIImage {
let controller = UIHostingController(rootView: self)
controller.view.bounds = CGRect(origin: .zero, size: size)
let image = controller.view.asImage()
return image
}
}
struct SnapshottingAPI_Previews: PreviewProvider {
static var previews: some View {
SnapshottingAPI()
}
}

I found a way to do it using some other posts. This is simple enough for me. Albeit its missing the thumbnail image in the ShareSheet
import SwiftUI
import LinkPresentation
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
struct SO1: View {
#State private var rect1: CGRect = .zero
#State private var uiimage: UIImage? = nil
#State private var isShareSheetShowing = false
var body: some View {
VStack {
HStack {
VStack {
Text("LEFT")
Text("VIEW")
}
.padding(20)
.background(Color.green)
.border(Color.blue, width: 5)
.background(RectGetter(rect: $rect1))
Button(action: shareButton){
Image(systemName: "square.and.arrow.up")
.frame(width: 35, height: 35)
}
}
}
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return ""
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return nil
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let image = UIImage(named: "YourImage")!
let imageProvider = NSItemProvider(object: image)
let metadata = LPLinkMetadata()
metadata.imageProvider = imageProvider
return metadata
}
func shareButton() {
isShareSheetShowing.toggle()
self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect1)
//let outputimage = Image(uiImage: self.uiimage!)
let items: [Any] = [self.uiimage!, self, "Test Text"]
let av = UIActivityViewController(activityItems: items, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController!.present(av, animated: true, completion: nil)
}
}
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)
}
}
struct SO1_Previews: PreviewProvider {
static var previews: some View {
SO1()
}
}

Related

How to use Photo library in SwiftUI

Xcode ver is 14.2.
Target iOS for 16.0.
I want to upload a image from photo library using PHPickerConfiguration().
It can't appear in the UserForm view.
struct UserForm: View {
#State var profileImage: Image?
#Binding var selectedImage: UIImage?
var body: some View {
if profileImage == nil{
Button(action:{
isShowAction.toggle()
}) {
Image(systemName: "person.crop.rectangle.badge.plus.fill")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.foregroundColor(.gray)
.padding(.vertical)
}
}else if let image = profileImage{
image
.resizable()
.scaledToFill()
.frame(width: 100,height: 100)
.clipShape(Circle())
.clipped()
.foregroundColor(.gray)
}
.sheet(isPresented: $imagePickerPresented, onDismiss: loadImage, content:{
AlbumPicker(image: $selectedImage)
}
)
}
}
extension UserForm{
func loadImage(){
guard let selectedImage = selectedImage else{ return }
profileImage = Image(uiImage: selectedImage)
}
}
import SwiftUI
import PhotosUI
struct AlbumPicker: UIViewControllerRepresentable {
#Binding var image: UIImage?
#Environment(\.presentationMode) var mode
func makeUIViewController(context: UIViewControllerRepresentableContext<AlbumPicker>) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 1
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: UIViewControllerRepresentableContext<AlbumPicker>) {}
class Coordinator: NSObject, UINavigationControllerDelegate, PHPickerViewControllerDelegate {
var parent: AlbumPicker
init(_ parent: AlbumPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
guard let itemProvider = results.first?.itemProvider else {
return
}
let typeChecked = itemProvider.registeredTypeIdentifiers.map { itemProvider.hasItemConformingToTypeIdentifier($0) }
guard !typeChecked.contains(false) else {
return
}
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { (url, error) in
guard let url = url else {
return
}
guard let imageData = try? Data(contentsOf: url) else {
return
}
self.parent.image = UIImage(data: imageData)
self.parent.mode.wrappedValue.dismiss()
}
}
}
}
When I push the button, immediately I got findWriterForTypeAndAlternateType:119: unsupported file format 'public.heic' using actual device.
And it can't hold profileImage in the view after I chose a image in photo library.
How does it be fixed?
Thank you

SwiftUI, Core Data, and PhotoKit: Views not updating when state changes (state management hell)

I'm learning Swift/SwiftUI by building a photo organizer app. It displays a user's photo library in a grid like the built-in photos app, and there's a detail view where you can do things like favorite a photo or add it to the trash.
My app loads all the data and displays it fine, but the UI doesn't update when things change. I've debugged enough to confirm that my edits are applied to the underlying PHAssets and Core Data assets. It feels like the problem is that my views aren't re-rendering.
I used Dave DeLong's approach to create an abstraction layer that separates Core Data from SwiftUI. I have a singleton environment object called DataStore that handles all interaction with Core Data and the PHPhotoLibrary. When the app runs, the DataStore is created. It makes an AssetFetcher that grabs all assets from the photo library (and implements PHPhotoLibraryChangeObserver). DataStore iterates over the assets to create an index in Core Data. My views' viewmodels query core data for the index items and display them using the #Query property wrapper from Dave's post.
App.swift
#main
struct LbPhotos2App: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.dataStore, DataStore.shared)
}
}
}
PhotoGridView.swift (this is what contentview presents)
struct PhotoGridView: View {
#Environment(\.dataStore) private var dataStore : DataStore
#Query(.all) var indexAssets: QueryResults<IndexAsset>
#StateObject var vm = PhotoGridViewModel()
func updateVm() {
vm.createIndex(indexAssets)
}
var body: some View {
GeometryReader { geo in
VStack {
HStack {
Text("\(indexAssets.count) assets")
Spacer()
TrashView()
}.padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
ScrollView {
ForEach(vm.sortedKeys, id: \.self) { key in
let indexAssets = vm.index[key]
let date = indexAssets?.first?.creationDate
GridSectionView(titleDate:date, indexAssets:indexAssets!, geoSize: geo.size)
}
}.onTapGesture {
updateVm()
}
}.onAppear {
updateVm()
}
.navigationDestination(for: IndexAsset.self) { indexAsset in
AssetDetailView(indexAsset: indexAsset)
}
}
}
}
PhotoGridViewModel.swift
class PhotoGridViewModel: ObservableObject {
#Published var index: [String:[IndexAsset]] = [:]
var indexAssets: QueryResults<IndexAsset>?
func createIndex() {
guard let assets = self.indexAssets else {return}
self.createIndex(assets)
}
func createIndex(_ queryResults: QueryResults<IndexAsset>) {
indexAssets = queryResults
if queryResults.count > 0 {
var lastDate = Date.distantFuture
for i in 0..<queryResults.count {
let item = queryResults[i]
let isSameDay = isSameDay(firstDate: lastDate, secondDate: item.creationDate!)
if isSameDay {
self.index[item.creationDateKey!]?.append(item)
} else {
self.index[item.creationDateKey!] = [item]
}
lastDate = item.creationDate!
}
}
self.objectWillChange.send()
}
var sortedKeys: [String] {
return index.keys.sorted().reversed()
}
private func isSameDay(firstDate:Date, secondDate:Date) -> Bool {
return Calendar.current.isDate(
firstDate,
equalTo: secondDate,
toGranularity: .day
)
}
}
Here's where I actually display the asset in GridSectionView.swift
LazyVGrid(columns: gridLayout, spacing: 2) {
let size = geoSize.width/4
ForEach(indexAssets, id:\.self) { indexAsset in
NavigationLink(
value: indexAsset,
label: {
AssetCellView(indexAsset: indexAsset, geoSize:geoSize)
}
).frame(width: size, height: size)
.buttonStyle(.borderless)
}
}
AssetCellView.swift
struct AssetCellView: View {
#StateObject var vm : AssetCellViewModel
var indexAsset : IndexAsset
var geoSize : CGSize
init(indexAsset: IndexAsset, geoSize: CGSize) {
self.indexAsset = indexAsset
self.geoSize = geoSize
_vm = StateObject(wrappedValue: AssetCellViewModel(indexAsset: indexAsset, geoSize: geoSize))
}
var body: some View {
ZStack(alignment: .bottomTrailing) {
if (vm.indexAsset != nil && vm.image != nil) {
vm.image?
.resizable()
.aspectRatio(contentMode: .fit)
.border(.blue, width: vm.indexAsset!.isSelected ? 4 : 0)
}
if (vm.indexAsset != nil && vm.indexAsset!.isFavorite) {
Image(systemName:"heart.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundStyle(.ultraThickMaterial)
.shadow(color: .black, radius: 12)
.offset(x:-8, y:-8)
}
}
}
}
AssetCellViewModel.swift
class AssetCellViewModel: ObservableObject{
#Environment(\.dataStore) private var dataStore
#Published var image : Image?
var indexAsset : IndexAsset?
var geoSize : CGSize
init(indexAsset: IndexAsset? = nil, geoSize:CGSize) {
self.indexAsset = indexAsset
self.geoSize = geoSize
self.requestImage(targetSize: CGSize(width: geoSize.width/4, height: geoSize.width/4))
}
func setIndexAsset(_ indexAsset:IndexAsset, targetSize: CGSize) {
self.indexAsset = indexAsset
self.requestImage(targetSize: targetSize)
}
func requestImage(targetSize: CGSize? = nil) {
if (self.indexAsset != nil) {
dataStore.fetchImageForLocalIdentifier(
id: indexAsset!.localIdentifier!,
targetSize: targetSize,
completionHandler: { image in
withAnimation(Animation.easeInOut (duration:0.15)) {
self.image = image
}
}
)
}
}
}
some of DataStore.swift
public class DataStore : ObservableObject {
static let shared = DataStore()
let persistenceController = PersistenceController.shared
#ObservedObject var assetFetcher = AssetFetcher()
let dateFormatter = DateFormatter()
var imageManager = PHCachingImageManager()
let id = UUID().uuidString
init() {
print("🔶 init dataStore: \(self.id)")
dateFormatter.dateFormat = "yyyy-MM-dd"
assetFetcher.iterateResults{ asset in
do {
try self.registerAsset(
localIdentifier: asset.localIdentifier,
creationDate: asset.creationDate!,
isFavorite: asset.isFavorite
)
} catch {
print("Error registering asset \(asset)")
}
}
}
func registerAsset(localIdentifier:String, creationDate:Date, isFavorite:Bool) throws {
let alreadyExists = indexAssetEntityWithLocalIdentifier(localIdentifier)
if alreadyExists != nil {
// print("🔶 Asset already registered: \(localIdentifier)")
// print(alreadyExists![0])
return
}
let iae = IndexAssetEntity(context: self.viewContext)
iae.localIdentifier = localIdentifier
iae.creationDate = creationDate
iae.creationDateKey = dateFormatter.string(from: creationDate)
iae.isFavorite = isFavorite
iae.isSelected = false
iae.isTrashed = false
self.viewContext.insert(iae)
try self.viewContext.save()
print("🔶 Registered asset: \(localIdentifier)")
}
And AssetFetcher.swift
class AssetFetcher:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
#Published var fetchResults : PHFetchResult<PHAsset>? = nil
let id = UUID().uuidString
override init() {
super.init()
print("🔶 init assetfetcher: \(id)")
self.startFetchingAllPhotos()
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func startFetchingAllPhotos() {
getPermissionIfNecessary(completionHandler: {result in
print(result)
})
let fetchOptions = PHFetchOptions()
var datecomponents = DateComponents()
datecomponents.month = -3
//TODO: request assets dynamically
let threeMonthsAgo = Calendar.current.date(byAdding: datecomponents, to:Date())
fetchOptions.predicate = NSPredicate(format: "creationDate > %# AND creationDate < %#", threeMonthsAgo! as NSDate, Date() as NSDate)
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.wantsIncrementalChangeDetails = true
// fetchOptions.fetchLimit = 1000
let results = PHAsset.fetchAssets(with: .image, options: fetchOptions)
PHPhotoLibrary.shared().register(self)
print("🔶 \(PHPhotoLibrary.shared())")
self.fetchResults = results
}
func iterateResults(_ callback:(_ asset: PHAsset) -> Void) {
print("iterateResults")
guard let unwrapped = self.fetchResults else {
return
}
for i in 0..<unwrapped.count {
callback(unwrapped.object(at: i))
}
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
print("🔶 photoLibraryDidChange")
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.fetchResults!) {
self.fetchResults = changeResults.fetchResultAfterChanges
// self.dataStore.photoLibraryDidChange(changeInstance)
// self.updateImages()
self.objectWillChange.send()
}
}
}
}

Should I pass viewModel or only model to view to upload image/Pdf Document in swiftui in MVVM?Do I need to keep progress for each document?

I have a situation Where I can add multiple images or videos URLs in Array 1 by 1. Similarly, a separate View (AssetView) is modified based on the array elements added. Now, the status of the Image/Video/PDF upload is changed and needs to reflect the progress upload and progress done. But here, I am unable to use MVVM.
I am confused about where should I call the upload function.
Should I pass viewModel to asset View or only view??
I am adding the Source code below to show my problem.
This is the first Screen where the user will get an option to show the card. On Card, the User can select Image/Video/Pdf any document.
struct ContentView: View {
#State var cardShown = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
cardShown.toggle()
}, label: {
Text("Show Card")
.bold()
.foregroundColor(Color.white)
.background(Color.blue)
.frame(width: 200, height: 50)
})
BottomCard(cardShown: $cardShown, height: 400, content: {
CardContent()
.padding()
})
}
}
}
}
This is the CardContentView, Where the user will add documents.
enum ActionType {
case ImageButtonAction
case VideoButtonAction
case None
}
struct CardContent: View {
#State private var text = ""
#State private var image: Image? = Image("UserProfilePlaceholder")
#State private var shouldPresentImagePicker = false
#State private var shouldPresentActionScheet = false
#State private var shouldPresentCamera = false
#State private var galleryAssetTypeSelected = GalleryAssetType.None
#State private var actionType = ActionType.None
#StateObject var messageAttachmentViewModel = MessageAttachmentViewModel()
// Document
#State private var shouldPresentDocumentPicker = false
var body: some View {
VStack {
Text("Photo Collage")
.bold()
.font(.system(size: 30))
.padding()
Text("You can create awesome photo grids and share them with all of your friends")
.font(.system(size: 18))
.multilineTextAlignment(.center)
TextEditor(text: $text)
.frame(height: 40)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(0..<self.messageAttachmentViewModel.commonMessageAttachmentModel.count, id: \.self) { i in
AssetView(messageAttachmentViewModel: messageAttachmentViewModel, index: i)
}
}
}
.background(Color.white)
.frame(height: 140)
HStack {
Button(action: {
self.shouldPresentActionScheet = true
self.actionType = .ImageButtonAction
}, label: {
Text("IMAGE")
})
Button(action: {
self.shouldPresentActionScheet = true
self.actionType = .VideoButtonAction
}, label: {
Text("VIDEO")
})
Button(action: {
self.galleryAssetTypeSelected = .PDF
self.shouldPresentDocumentPicker = true
}, label: {
Text("PDF")
})
Spacer()
Text("500")
.font(.system(size: 18))
Button(action: {
}, label: {
Text("SEND")
})
}
}
.padding()
.sheet(isPresented: $shouldPresentImagePicker) {
ImagePicker(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, image: self.$image, isPresented: self.$shouldPresentImagePicker, galleryAssetType: $galleryAssetTypeSelected, messageAttachmentViewModel: messageAttachmentViewModel)
}.actionSheet(isPresented: $shouldPresentActionScheet) { () -> ActionSheet in
ActionSheet(title: Text("Choose mode"), message: Text("Please choose your preferred mode to set your profile image"), buttons: [ActionSheet.Button.default(Text("Camera"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
self.galleryAssetTypeSelected = .None
}), ActionSheet.Button.default(Text(actionType == ActionType.ImageButtonAction ? "Photo Library" : "Video"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
self.galleryAssetTypeSelected = (actionType == ActionType.ImageButtonAction) ? GalleryAssetType.Photo : GalleryAssetType.Video
self.galleryAssetTypeSelected = actionType == ActionType.ImageButtonAction ? .Photo : .Video
}), ActionSheet.Button.cancel()])
}
// .sheet(isPresented: $shouldPresentDocumentPicker) {
// DocumentPicker(isDocumentPickerPresented: $shouldPresentDocumentPicker, galleryAssetType: $galleryAssetTypeSelected, commentAttachments: $commentAttachments)
// }
}
}
Below is Image Picker Struct to select Image/Video from Gallery.
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var image: Image?
#Binding var isPresented: Bool
#Binding var galleryAssetType: GalleryAssetType
#ObservedObject var messageAttachmentViewModel: MessageAttachmentViewModel
func makeCoordinator() -> ImagePickerViewCoordinator {
return ImagePickerViewCoordinator(image: $image, isPresented: $isPresented, galleryAssetType: $galleryAssetType, messageAttachmentViewModel: messageAttachmentViewModel)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let pickerController = UIImagePickerController()
pickerController.sourceType = sourceType
pickerController.delegate = context.coordinator
if galleryAssetType == .Photo {
pickerController.mediaTypes = ["public.image"]
} else if galleryAssetType == .Video {
pickerController.mediaTypes = ["public.movie"]
pickerController.videoQuality = .typeHigh
}
return pickerController
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
// Nothing to update here
}
}
class ImagePickerViewCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var image: Image?
#Binding var isPresented: Bool
#Binding var galleryAssetType: GalleryAssetType
#ObservedObject var messageAttachmentViewModel: MessageAttachmentViewModel
init(image: Binding<Image?>, isPresented: Binding<Bool>, galleryAssetType: Binding<GalleryAssetType>, messageAttachmentViewModel: MessageAttachmentViewModel) {
self._image = image
self._isPresented = isPresented
self._galleryAssetType = galleryAssetType
self.messageAttachmentViewModel = messageAttachmentViewModel
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
self.image = Image(uiImage: image)
}
if galleryAssetType == .Photo {
if let imageURL = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerImageURL") ] as? URL {
let image = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerOriginalImage")] as? UIImage
let messageAttachmentModel = MessageAttachmentModel(assetType: .Photo, assetUrl: imageURL, image: image, uploadStatus: false)
self.messageAttachmentViewModel.commonMessageAttachmentModel.append(messageAttachmentModel)
}
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
self.image = Image(uiImage: image)
}
} else if galleryAssetType == .Video {
if let videoURL = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL") ] as? URL {
let messageAttachmentModel = MessageAttachmentModel(assetType: .Video, assetUrl: videoURL, uploadStatus: false)
self.messageAttachmentViewModel.commonMessageAttachmentModel.append(messageAttachmentModel)
}
}
self.isPresented = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.isPresented = false
}
}
This is VideoThumnail View to show only thumbnail after selection. The actual video has to be uploaded to the server.
struct VideoThumbnail: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var videoThumbnail = UIImage()
var state = LoadState.loading
init(url: URL) {
if url.pathComponents.isEmpty {
self.state = .failure
return
}
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
avAssetImageGenerator.maximumSize = CGSize(width: 150, height: 150)
let thumnailTime = CMTimeMake(value: 2, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumnailTime, actualTime: nil)
self.videoThumbnail = UIImage(cgImage: cgThumbImage)
self.state = .success
} catch {
print(error.localizedDescription)
self.state = .failure
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
.aspectRatio(contentMode: .fit)
}
init(url: URL, loading: Image = Image(systemName: "photo"), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
return Image(uiImage: loader.videoThumbnail)
}
}
}
Below is PDFThumbnail View.
struct PdfThumbnailView: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var pdfThumbnail = UIImage()
var state = LoadState.loading
init(url: URL) {
if url.pathComponents.isEmpty {
self.state = .failure
return
}
let pdfDocument = PDFDocument(url: url)
if let pdfDocumentPage = pdfDocument?.page(at: 1) {
pdfThumbnail = pdfDocumentPage.thumbnail(of: CGSize(width: 150, height: 150), for: PDFDisplayBox.trimBox)
self.state = .success
} else {
self.state = .failure
}
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
.aspectRatio(contentMode: .fit)
}
init(url: URL, loading: Image = Image(systemName: "photo"), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
return Image(uiImage: loader.pdfThumbnail)
}
}
}
MessageAttachmentModel: This Model is created when Image/Video/Pdf is selected.
struct MessageAttachmentModel {
var assetType = GalleryAssetType.None
var assetUrl: URL
var image: UIImage?
var uploadStatus: Bool
init(assetType: GalleryAssetType, assetUrl: URL, image: UIImage? = nil, uploadStatus: Bool) {
self.assetType = assetType
self.assetUrl = assetUrl
self.image = image
self.uploadStatus = uploadStatus
}
}
MessageAttachmentModelView: This ModelView contains an array of MessageAttachmentModel as a published property to reflect the change.
class MessageAttachmentViewModel: ObservableObject {
#Published var commonMessageAttachmentModel: [MessageAttachmentModel] = []
#Published var isUploadedLeft: Bool = false
func getIsUploadedStatus() {
let leftToUpload = commonMessageAttachmentModel.filter({ $0.uploadStatus == false })
isUploadedLeft = (leftToUpload.count > 0) ? true : false
}
func updateData() {
for var model in commonMessageAttachmentModel {
if model.uploadStatus == false {
if let endUsedId = getEndUserDataId(), let data = getDataFromURL(url: model.assetUrl) {
let timestamp = Date().timeIntervalSince1970
let key = "u_me_\(endUsedId)_\(timestamp))"
var assetType = (model.assetType == .Photo) ? ("Image") : ((model.assetType == .Video) ? "Video" : "Files")
uploadFileData(assetType: assetType, key: key, data: data) { status, urlString in
if status {
model.uploadStatus = true
}
}
}
}
}
}
func uploadFileData(assetType: String, key: String, data: Data , completion: #escaping (Bool, String) -> Void ) {
/// Server Data Upload
}
func getEndUserDataId() -> String? {
var endUserId: String?
return "5"
}
I have to show the progress of Image/Video/Pdf upload on Asset View. I am unable to identify how can I achieve it.
I am adding a simulator screenshot also to understand the situation clearly.
I am struggling to identify that do I need to keep progress for each document? Please help.

How to get the image from image picker and display into a add form?

I have the struct below to handle the image picker but I'm unable to pass the image selected to the view to be displayed. Selected path below prints correctly location of the file but how assign this selected file to the pic into the view ?
struct FileView: View {
var body: some View {
Button("Select File") {
let openPanel = NSOpenPanel()
openPanel.prompt = "Select File"
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = true
openPanel.allowedFileTypes = ["png","jpg","jpeg"]
openPanel.begin { (result) -> Void in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
let selectedPath = openPanel.url!.path
print(selectedPath)
}
}
}
}
}
Below the place where I display the selected image:
struct NewPost: View {
#Environment(\.presentationMode) var presentationMode
var postToEdit: Item?
var viewContext: NSManagedObjectContext
...
#State var fileUrl: URL?
var header: String {
postToEdit == nil ? "Create Post" : "Edit Post"
}
var body: some View {
...Form Code
// Displaying Image if its selected...
Section(header: Text("Picture").foregroundColor(Color("blue")).font(.title2)) {
if let fileUrl = fileUrl, let img = NSImage(contentsOf: fileUrl) {
Image(nsImage: img)
.resizable()
.scaledToFill()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.opacity(1)
.frame(minWidth: 120, maxWidth: 120, minHeight: 120, maxHeight: 120)
}
if pic.count != 0 {
Image(nsImage: NSImage(data: pic)!)
.resizable()
.scaledToFill()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.opacity(1)
.frame(minWidth: 120, maxWidth: 120, minHeight: 120, maxHeight: 120)
}
}
.cornerRadius(10)
Updated code above based on response below.
Thanks in advance.
You can use a Binding to pass data from a child view back up to a parent:
struct FileView: View {
#Binding var fileUrl : URL?
var body: some View {
Button("Select File") {
let openPanel = NSOpenPanel()
openPanel.prompt = "Select File"
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = true
openPanel.allowedFileTypes = ["png","jpg","jpeg"]
openPanel.begin { (result) -> Void in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
fileUrl = openPanel.url
}
}
}
}
}
struct ContentView: View {
#State var fileUrl: URL?
var body: some View {
VStack {
FileView(fileUrl: $fileUrl)
if let fileUrl = fileUrl, let image = NSImage(contentsOf: fileUrl) {
Image(nsImage: image)
}
}
}
}
Notice that in the child, it's #Binding, but in the parent, it's #State.
Updated version, using Data:
struct FileView: View {
#Binding var fileData : Data?
var body: some View {
Button("Select File") {
let openPanel = NSOpenPanel()
openPanel.prompt = "Select File"
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = true
openPanel.allowedFileTypes = ["png","jpg","jpeg"]
openPanel.begin { (result) -> Void in
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
guard let url = openPanel.url, let data = try? Data(contentsOf: url) else {
//handle errors here
return
}
fileData = data
}
}
}
}
}
struct ContentView: View {
#State var fileData: Data?
var body: some View {
VStack {
FileView(fileData: $fileData)
if let fileData = fileData, let image = NSImage(data: fileData) {
Image(nsImage: image)
}
}
}
}
Note: obviously, not doing any error handling for not being able to read from the URL or anything in this example

SwiftUI can't get image from download url

I have the following code to load an image from a download url and display it as an UIImage.
I expected it to work, but somehow, solely the placeholder image 'ccc' is being displayed, and not the actual image from the download url. How so?
My urls are being fetched from a database and kind of look like this:
https://firebasestorage.googleapis.com/v0/b/.../o/P...alt=media&token=...-579f...da
struct ShelterView: View {
var title: String
var background: String
var available: Bool
var distance: Double
var gender: String
#ObservedObject private var imageLoader: Loader
init(title: String, background: String, available: Bool, distance: Double, gender: String) {
self.title = title
self.background = background
self.available = available
self.distance = distance
self.gender = gender
self.imageLoader = Loader(background)
}
var image: UIImage? {
imageLoader.data.flatMap(UIImage.init)
}
var body: some View {
VStack {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 0) {
Text(title)
.font(Font.custom("Helvetica Now Display Bold", size: 30))
.foregroundColor(.white)
.padding(15)
.lineLimit(2)
HStack(spacing: 25) {
IconInfo(image: "bed.double.fill", text: String(available), color: .white)
if gender != "" {
IconInfo(image: "person.fill", text: gender, color: .white)
}
}
.padding(.leading, 15)
}
Spacer()
IconInfo(image: "mappin.circle.fill", text: String(distance) + " miles away", color: .white)
.padding(15)
}
Spacer()
}
.background(
Image(uiImage: image ?? UIImage(named: "ccc")!) <-- HERE
.brightness(-0.11)
.frame(width: 255, height: 360)
)
.frame(width: 255, height: 360)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
}
final class Loader: ObservableObject {
var task: URLSessionDataTask!
#Published var data: Data? = nil
init(_ urlString: String) {
print(urlString)
let url = URL(string: urlString)
task = URLSession.shared.dataTask(with: url!, completionHandler: { data, _, _ in
DispatchQueue.main.async {
self.data = data
}
})
task.resume()
}
deinit {
task.cancel()
}
}
Your image is a plain old var which happens to be nil when the View is built. SwiftUI only rebuilds itself in response to changes in #ObservedObject, #State, or #Binding, so move your image to an #Published property on your imageLoader and it will work. Here is my caching image View:
import SwiftUI
import Combine
import UIKit
class ImageCache {
enum Error: Swift.Error {
case dataConversionFailed
case sessionError(Swift.Error)
}
static let shared = ImageCache()
private let cache = NSCache<NSURL, UIImage>()
private init() { }
static func image(for url: URL?) -> AnyPublisher<UIImage?, ImageCache.Error> {
guard let url = url else {
return Empty().eraseToAnyPublisher()
}
guard let image = shared.cache.object(forKey: url as NSURL) else {
return URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { (tuple) -> UIImage in
let (data, _) = tuple
guard let image = UIImage(data: data) else {
throw Error.dataConversionFailed
}
shared.cache.setObject(image, forKey: url as NSURL)
return image
}
.mapError({ error in Error.sessionError(error) })
.eraseToAnyPublisher()
}
return Just(image)
.mapError({ _ in fatalError() })
.eraseToAnyPublisher()
}
}
class ImageModel: ObservableObject {
#Published var image: UIImage? = nil
var cacheSubscription: AnyCancellable?
init(url: URL?) {
cacheSubscription = ImageCache
.image(for: url)
.replaceError(with: nil)
.receive(on: RunLoop.main, options: .none)
.assign(to: \.image, on: self)
}
}
struct RemoteImage : View {
#ObservedObject var imageModel: ImageModel
private let contentMode: ContentMode
init(url: URL?, contentMode: ContentMode = .fit) {
imageModel = ImageModel(url: url)
self.contentMode = contentMode
}
var body: some View {
imageModel
.image
.map { Image(uiImage:$0).resizable().aspectRatio(contentMode: contentMode) }
?? Image(systemName: "questionmark").resizable().aspectRatio(contentMode: contentMode)
}
}