Load NSImage into Image(named:) in SwiftUI - swift

I have a MacOS app that I am rewriting in SwiftUI. I am completely new to SwiftUI.
I have an Image() and when I drag an image from the Desktop on to that image I want to load the dropped image into it. I am able to detect drop and I am able to switch out the images to show the cursor is hovering over the drop zone. But, what I don't understand is how can I take the file from the desktop and load it into the Image()?
I have the file's URL, I create an NSImage from the contentsOf but how do I get that NSImage into the Image(named:)?
"input" & "inputDropZone" are assets in the asset catalog
Is my approach wrong?
struct ContentView: View {
#State var sourceImage = "input"
var body: some View {
VStack {
Image(sourceImage)
.frame(width: 200.0, height: 200.0)
.onDrop(of: [UTType.fileURL.description], delegate: self)
}
}
}
extension ContentView : DropDelegate {
func performDrop(info: DropInfo) -> Bool {
guard let itemProvider = info.itemProviders(for: [UTType.fileURL.description]).first else { return false }
itemProvider.loadItem(forTypeIdentifier: UTType.fileURL.description) { item, error in
guard let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) else { return }
if let image = NSImage(contentsOf: url) {
DispatchQueue.main.async {
sourceImage = ? //How do I load an NSImage into an Image(named:)?
}
}
}
return true
}
func dropEntered(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = "inputDropZone"
}
}
func dropExited(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = "input"
}
}
}

I got this to work by taking Asperi's & Vadian's suggestions. I combined Asperi's answer at:
https://stackoverflow.com/a/60832686/12299030
into my own code so that I could use the delegate methods. This is the final working code:
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
#State var sourceImage = NSImage(named: "input")
var body: some View {
VStack {
Image(sourceImage)
.frame(width: 200.0, height: 200.0)
.onDrop(of: [UTType.fileURL.description], delegate: self)
}
}
}
extension ContentView : DropDelegate {
func performDrop(info: DropInfo) -> Bool {
guard let providers = info.itemProviders(for: [UTType.fileURL.description]).first else { return false }
providers.loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler: { (data, error) in
if let data = data, let path = NSString(data: data, encoding: 4), let url = URL(string: path as String) {
let image = NSImage(contentsOf: url)
image?.size = CGSize(width: 200.0, height: 200.0)
DispatchQueue.main.async {
self.sourceImage = image
}
}
})
return true
}
func dropEntered(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = NSImage(named: "inputDropZone")
}
}
func dropExited(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = NSImage(named: "input")
}
}
}

Related

How to call a function on state change

I want to update the image in firebase storage when my State changes. Currently I call my function to upload the picked image to storage in the onAppear:
#State var shouldShowImagePicker: Bool = false
#State var image: UIImage?
Button(action: {
shouldShowImagePicker.toggle()
}) {
if let image = self.image {
Image(uiImage: image)
.resizable()
.frame(maxWidth: 300, maxHeight: 300)
.scaledToFit()
.onAppear() {
persistImageToStorage()
}
} else {
Text("Select Image")
.frame(width: 300, height: 300)
}
}
.fullScreenCover(isPresented: $shouldShowImagePicker, onDismiss: nil) {
ImagePicker(image: $image)
}
This is the function to upload the image to storage:
private func persistImageToStorage() {
let ref = FirebaseManager.shared.storage.reference(withPath: "image")
guard let imageData = self.image?.jpegData(compressionQuality: 0.5)
else { return }
ref.putData(imageData, metadata: nil) { metadata, err in
if let err = err {
print(err)
return
}
ref.downloadURL { url, err in
if let err = err {
print(err)
return
}
}
}
}
This is my ImagePicker component:
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
#Binding var image: UIImage?
private let controller = UIImagePickerController()
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
parent.image = info[.originalImage] as? UIImage
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
}
func makeUIViewController(context: Context) -> some UIViewController {
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Doing it this way works fine the first time I open the ImagePickerbut once an image is selected it does not call the function again since the onAppear does not call again.
I tried setting onChange on the binding of the image but it seemed to not work for me.
You could create a Binding that explicitly persists the image when the value is set:
.fullScreenCover(isPresented: $shouldShowImagePicker, onDismiss: nil) {
let imageBinding = Binding(
get: { image },
set: { value in
image = value
persistImageToStorage()
}
)
ImagePicker(image: imageBinding)
}
Note: Since imageBinding is already a Binding, you do not use a $ here.
Tested in Xcode 13.4.1
You said using onChange with image did not work for you, but I also found this to work:
ImagePicker(image: $image)
.onChange(of: image) { _ in
persistImageToStorage()
}

Best practice for wrapping `AVPlayerView` in `SwiftUI`?

I want to wrap AVPlayerView into SwiftUI. Here are my codes(playground):
import PlaygroundSupport
import SwiftUI
import AVKit
class RecorderPlayerModel: ObservableObject {
#Published var playerView: AVPlayerView
init() {
self.playerView = AVPlayerView()
self.playerView.player = AVPlayer()
}
func reload(url: URL) {
let asset = AVAsset(url: url)
let item = AVPlayerItem(asset: asset)
self.playerView.player?.replaceCurrentItem(with: item)
}
}
struct RecorderPlayerView: NSViewRepresentable {
typealias NSViewType = AVPlayerView
var playerView: AVPlayerView
func makeNSView(context: Context) -> AVPlayerView {
return playerView
}
func updateNSView(_ nsView: AVPlayerView, context: Context) {}
}
struct ContentView: View {
#StateObject var playerViewModel: RecorderPlayerModel = .init()
var body: some View {
VStack {
RecorderPlayerView(playerView: playerViewModel.playerView)
.clipShape(RoundedRectangle(cornerRadius: 8))
.onAppear {
let fileManager = FileManager.default
if let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4") {
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
let fileUrl = fileManager.temporaryDirectory.appendingPathComponent("sample")
.appendingPathExtension(for: .mpeg4Movie)
try? fileManager.removeItem(at: fileUrl)
fileManager.createFile(atPath: fileUrl.path, contents: data)
playerViewModel.reload(url: fileUrl)
} catch {
print(error)
}
}
}
}
Button {
if playerViewModel.playerView.canBeginTrimming {
Task {
await playerViewModel.playerView.beginTrimming()
}
}
} label: {
Text("trim")
}
}.frame(width: 500, height: 500, alignment: .center)
}
}
PlaygroundPage.current.setLiveView(ContentView())
Since I want to trim the video, I cannot directly use VideoPlayer. But after wrapping AVPlayerView to NSViewRepresentable View, the trim view always lose interactivity.
Reproduce way: just double click at anywhere when trimming.
supplement
when losing interactivity, the console will log
-[AVTrimIndicatorAccessibilityElement accessibilityHitTest:]: unrecognized selector sent to instance 0x600001916ce0
updated
It is not triggered on all models of Mac.
I figure out the problem. It seems that applying .clipShape(...) on RecorderPlayerView will cause the problem.
Just remove the line .clipShape(RoundedRectangle(cornerRadius: 8)) will solve this problem.
RecorderPlayerView(playerView: playerViewModel.playerView)
// .clipShape(RoundedRectangle(cornerRadius: 8)) <-- remove this line
.onAppear {
...
}

SwiftUI User selects image and saves it within app to be retrieved later

I need to allow the user to select the image they want to save to the app that will be retrieved later. I already have the photo picker within the code, I just don’t know how to save and retrieve the image.
struct PhotoPicker: UIViewControllerRepresentable {
#Binding var Badge: UIImage
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.allowsEditing = true
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(photoPicker: self)
}
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let photoPicker: PhotoPicker
init(photoPicker: PhotoPicker){
self.photoPicker = photoPicker
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.editedImage] as? UIImage{
photoPicker.Badge = image
BadgeStatus.toggle()
}
picker.dismiss(animated: true)
}
}
}
here is some code that writes an image to file, then reads it again.
From this you should be able to "... save and retrieve the image."
struct ContentView: View {
#State var image = UIImage(systemName: "globe")! // <-- test image
#State var fileURL: URL?
var body: some View {
VStack (spacing: 55) {
Button(action: { saveImage() }) { // <-- first save the image to file
Text("1. write image to file")
}
Button(action: { image = UIImage() }) { // <-- second clear the image from the view
Text("2. clear image")
}
Button(action: { image = loadImage() }) { // <-- third read the image from file
Text("3. read image from file")
}
Image(uiImage: image)
}
}
func saveImage() {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("imageFile")
.appendingPathExtension("png")
fileURL = furl
try image.pngData()?.write(to: furl)
} catch {
print("could not create imageFile")
}
}
func loadImage() -> UIImage {
do {
if let furl = fileURL {
let data = try Data(contentsOf: furl)
if let img = UIImage(data: data) {
return img
}
}
} catch {
print("error: \(error)") // todo
}
return UIImage()
}
}

How can I remove a image from the cache with Combine in SwiftUI?

When I delete the data from array, the operation is successful, but only the picture does not change. The image still remains in the cache. However, when I close and open the application, the application works fine.
how can i update the cache?
Image Loader
import Combine
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private let url: String
private var cancellable: AnyCancellable?
private var cache: ImageCache?
init(url: String, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard let cacheURL = URL(string: url) else { return }
if let image = cache?[cacheURL] {
self.image = image
return
}
guard let url = URL(string: url) else { return }
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveOutput: { [weak self] in self?.cache($0) })
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
private func cache(_ image: UIImage?) {
guard let cacheURL = URL(string: url) else { return }
image.map { cache?[cacheURL] = $0 }
}
func cancel() {
cancellable?.cancel()
}
}
Async Image
import Combine
struct AsyncImage<Placeholder: View>: View {
#StateObject private var loader: ImageLoader
private let placeholder: Placeholder
init(url: String, #ViewBuilder placeholder: () -> Placeholder) {
self.placeholder = placeholder()
_loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
}
var body: some View {
content
.onAppear(perform: loader.load)
}
private var content: some View {
Group {
if loader.image != nil {
Image(uiImage: loader.image!)
.resizable()
} else {
placeholder
}
}
}
}
Image Cache
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
Temporary Image Cache
struct TemporaryImageCache: ImageCache {
private let cache = NSCache<NSURL, UIImage>()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
Image Cache Key
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}
Async Image Using
VStack {
ZStack(alignment: .bottomTrailing) {
AsyncImage(url: "image url") {
Text("Loading")
}
.aspectRatio(contentMode: .fill)
.frame(width: 120, height: 180, alignment: .center)
.cornerRadius(15)
....
}
}

Updating image view in SwiftUI after downloading image

On the click of a button I am trying to download a new random image and update the view. When the app loads it displays the downloaded image. When the button is clicked the image seems to download but the view is never updated and displays the place holder image. Am I missing something here, any ideas? Here is a simplified version.
import SwiftUI
struct ContentView : View {
#State var url = "https://robohash.org/random.png"
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in:0 ..< 10)).png"
}) {
Text("Get Random Robot Image")
}
URLImage(url: url)
}
}
}
class ImageLoader: BindableObject {
var downloadedImage: UIImage?
let didChange = PassthroughSubject<ImageLoader?, Never>()
func load(url: String) {
guard let imageUrl = URL(string: url) else {
fatalError("Image URL is not correct")
}
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
self.didChange.send(nil)
}
return
}
self.downloadedImage = UIImage(data: data)
DispatchQueue.main.async {
print("downloaded image")
self.didChange.send(self)
}
}.resume()
}
}
import SwiftUI
struct URLImage : View {
#ObjectBinding private var imageLoader = ImageLoader()
var placeholder: Image
init(url: String, placeholder: Image = Image(systemName: "photo")) {
self.placeholder = placeholder
self.imageLoader.load(url: url)
}
var body: some View {
if let uiImage = self.imageLoader.downloadedImage {
print("return downloaded image")
return Image(uiImage: uiImage)
} else {
return placeholder
}
}
}
The problem seems to be related to some kind of lost synchronization between the ContentView and the ImageURL (that happens after the button click event).
A possible workaround is making the ImageURL a #State property of the ContentView.
After that, inside the scope of the button click event, we can call the image.imageLoader.load(url: ) method. As the download of the image ends, the publisher (didChange) will notify the ImageURL and then the change is correctly propagated to the ContentView.
import SwiftUI
import Combine
enum ImageURLError: Error {
case dataIsNotAnImage
}
class ImageLoader: BindableObject {
/*
init(url: URL) {
self.url = url
}
private let url: URL */
let id: String = UUID().uuidString
var didChange = PassthroughSubject<Void, Never>()
var image: UIImage? {
didSet {
DispatchQueue.main.async {
self.didChange.send()
}
}
}
func load(url: URL) {
print(#function)
self.image = nil
URLSession.shared.dataTask(with: url) { (data, res, error) in
guard error == nil else {
return
}
guard
let data = data,
let image = UIImage(data: data)
else {
return
}
self.image = image
}.resume()
}
}
URLImage view:
struct URLImage : View {
init() {
self.placeholder = Image(systemName: "photo")
self.imageLoader = ImageLoader()
}
#ObjectBinding var imageLoader: ImageLoader
var placeholder: Image
var body: some View {
imageLoader.image == nil ?
placeholder : Image(uiImage: imageLoader.image!)
}
}
ContentView:
struct ContentView : View {
#State var url: String = "https://robohash.org/random.png"
#State var image: URLImage = URLImage()
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in: 0 ..< 10)).png"
self.image.imageLoader.load(url: URL(string: self.url)!)
}) {
Text("Get Random Robot Image")
}
image
}
}
}
Anyway I will try to investigate the problem and if I will know something new I will modify my answer.