SwiftUI: How to Call a Function when Photo Picker Closes - swift

I'm using a photo picker in my SwiftUI class to load photos and videos into an array. Right now I'm displaying those images after they've been selected in the picker. Works fine.
Instead, I'd like to run a function when I click the "Add" button and upload the objects in that array to Cloudinary for processing and storage. I can manually make this happen with a separate button outside the picker, but for the best UX, I think this function should run automatically when that "Add" button is selected.
How do I run a function when that "Add" button is clicked? Do I need to check if the array is not empty and some other condition exists instead?
Here's an image of the picker:
Here's the picker code:
import SwiftUI
import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
#ObservedObject var mediaItems: PickedMediaItems
var didFinishPicking: (_ didSelectItems: Bool) -> Void
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.filter = .any(of: [.images, .videos, .livePhotos])
config.selectionLimit = 0
config.preferredAssetRepresentationMode = .current
let controller = PHPickerViewController(configuration: config)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(with: self)
}
class Coordinator: PHPickerViewControllerDelegate {
var photoPicker: PhotoPicker
init(with photoPicker: PhotoPicker) {
self.photoPicker = photoPicker
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
photoPicker.didFinishPicking(!results.isEmpty)
guard !results.isEmpty else {
return
}
for result in results {
let itemProvider = result.itemProvider
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first,
let utType = UTType(typeIdentifier)
else { continue }
if utType.conforms(to: .image) {
self.getPhoto(from: itemProvider, isLivePhoto: false)
} else if utType.conforms(to: .movie) {
self.getVideo(from: itemProvider, typeIdentifier: typeIdentifier)
} else {
self.getPhoto(from: itemProvider, isLivePhoto: true)
}
}
}
private func getPhoto(from itemProvider: NSItemProvider, isLivePhoto: Bool) {
let objectType: NSItemProviderReading.Type = !isLivePhoto ? UIImage.self : PHLivePhoto.self
if itemProvider.canLoadObject(ofClass: objectType) {
itemProvider.loadObject(ofClass: objectType) { object, error in
if let error = error {
print(error.localizedDescription)
}
if !isLivePhoto {
if let image = object as? UIImage {
DispatchQueue.main.async {
self.photoPicker.mediaItems.append(item: PhotoPickerModel(with: image))
}
}
} else {
if let livePhoto = object as? PHLivePhoto {
DispatchQueue.main.async {
self.photoPicker.mediaItems.append(item: PhotoPickerModel(with: livePhoto))
}
}
}
}
}
}
private func getVideo(from itemProvider: NSItemProvider, typeIdentifier: String) {
itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in
if let error = error {
print(error.localizedDescription)
}
guard let url = url else { return }
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
guard let targetURL = documentsDirectory?.appendingPathComponent(url.lastPathComponent) else { return }
do {
if FileManager.default.fileExists(atPath: targetURL.path) {
try FileManager.default.removeItem(at: targetURL)
}
try FileManager.default.copyItem(at: url, to: targetURL)
DispatchQueue.main.async {
self.photoPicker.mediaItems.append(item: PhotoPickerModel(with: targetURL))
}
} catch {
print(error.localizedDescription)
}
}
}
}
}

I added this class to the View:
class PickerStatus: ObservableObject {
var status: Bool = false
}
Then added this line to the PhotoPicker:
#ObservedObject var finishedSelection: PickerStatus
Then in the Coordinator, I added this:
for result in results {
self.photoPicker.finishedSelection.status = true
...
}
Now in my View I can set the instance of the ObservedObject, pass it into my child views including the PhotoPicker and check the value of that same Observed Object:
#ObservedObject var finishedSelection = PickerStatus()

Related

How to load next Image while user is looking at first image in the TabView pages carousel with AsyncImageView In SwiftUI?

I have an images carousel that fetching heavy images from a few URLs and displaying asynchronously when user is going to the next page. And the problem is that user need to see a waiting wheel and wait to see the next image.
So the ideal case would be to eliminate the waiting wheel by loading the next image while user still seeing the fist image and I don't know how to do it properly.
If you know the solution please let me know, I will mean a lot to me.
Here's the code:
struct ContentView: View {
#State private var selectedImageToSee: Int = 0
private let imagesUrl: [String] = [
"https://images.unsplash.com/photo-1633125195723-04860dde4545?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDUyMQ&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1633090332452-532d6b39422a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDI1Ng&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1591667954789-c25d5affec59?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzU1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1580316576539-aee1337e80e8?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzk5&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1484976063837-eab657a7aca7?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNjky&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024"
]
var body: some View {
ImagesCaruselView(selectedImageToSee: $selectedImageToSee, urls: imagesUrl)
}
}
struct ImagesCaruselView: View {
#Binding var selectedImageToSee: Int
let urls: [String]
var body: some View {
TabView(selection: $selectedImageToSee) {
ForEach(0..<urls.count, id: \.self) { url in
AsyncImageView(
url: URL(string: urls[url])!,
placeholder: { ProgressView().scaleEffect(1.5).progressViewStyle(CircularProgressViewStyle(tint: .gray)) },
image: { Image(uiImage: $0) }
)
.padding()
}
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
struct AsyncImageView<Placeholder: View>: View {
#StateObject private var loader: ImageLoader
private let placeholder: Placeholder
private let image: (UIImage) -> Image
init(
url: URL,
#ViewBuilder placeholder: () -> Placeholder,
#ViewBuilder image: #escaping (UIImage) -> Image = Image.init(uiImage:)
) {
self.placeholder = placeholder()
self.image = image
_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(loader.image!)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
placeholder
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache: NSCache<NSURL, UIImage> = {
let cache = NSCache<NSURL, UIImage>()
cache.countLimit = 100 // 100 items
cache.totalCostLimit = 1024 * 1024 * 200 // 200 MB
return cache
}()
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) }
}
}
import Combine
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private(set) var isLoading = false
private let url: URL
private var cache: ImageCache?
private var cancellable: AnyCancellable?
private static let imageProcessingQueue = DispatchQueue(label: "image-processing")
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard !isLoading else { return }
if let image = cache?[url] {
self.image = image
return
}
#warning("Caching the image are disabled")
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
// receiveOutput: { [weak self] in self?.cache($0) },
receiveCompletion: { [weak self] _ in self?.onFinish() },
receiveCancel: { [weak self] in self?.onFinish() })
.subscribe(on: Self.imageProcessingQueue)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
func cancel() {
cancellable?.cancel()
}
private func onStart() {
isLoading = true
}
private func onFinish() {
isLoading = false
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
}
import SwiftUI
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}

How do I bind a ViewModel to a Collationview?

I'm trying to bind a view model to a collection view. But I don't know how to do it. I'm using MVVM pattern and RxSwift, and I've only tried table view binding before. Here's my view model and the view controller code I've done so far.
class SearchViewModel: ViewModelType {
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
This is my ViewModel code
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
}
And this is my ViewController code.
How should the collection view bind?
Here is what your view model should look like:
class SearchViewModel {
// no need for a disposedBag. If you are putting a disposeBag in your view model, you are likely doing something wrong.
struct input {
let loadData: Signal<Void>
}
struct output {
let loadApplyList: Driver<[User]> // you should be passing an array here, not an object.
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let friendResult = input.loadData
.flatMapLatest {
api.getFriend()
.compactMap { $0.0.map(Result<friends, Error>.success) }
.asDriver(onErrorRecover: { Driver.just(Result<friends, Error>.failure($0)) })
}
let loadApplyList = friendResult
.compactMap { (result) -> [User]? in
guard case let .success(list) = result else { return nil }
return list.friends
}
return output(loadApplyList: loadApplyList)
}
}
Now in your view controller, you can bind it like this:
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.loadApplyList
.drive(collectionView.rx.items(cellIdentifier: "Cell", cellType: MyCellType.self)) { index, item, cell in
// configure cell with item here
}
.disposed(by: disposeBag)
}

I can print data but can't assign it to a label in Swift

I sent my data from my API call to my InfoController viewDidLoad. There, I was able to safely store it in a skillName constant, and also printed it, receiving all the information by console.
The problem comes when I try to assign this variable to my skillLabel.
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
guard var skillName = names as? String else { return }
self.pokemon?.skillName = skillName
self.allNames = skillName
print(self.allNames)
}
}
There, when I print allNames, the console shows all the data I need. This is how the data looks like: Data Example
And the computed property where I wanna use this data looks is:
var pokemon: Pokemon? {
didSet {
guard let id = pokemon?.id else { return }
guard let data = pokemon?.image else { return }
navigationItem.title = pokemon?.name?.capitalized
infoLabel.text = pokemon?.description
infoView.pokemon = pokemon
if id == pokemon?.id {
imageView.image = UIImage(data: data)
infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: "\(allNames)")
}
}
}
PD: allNames is a String variable I have at InfoController class-level.
This is how my app looks when run:
PokeApp
My goal is to get that details param to show the skillName data, but it returns nil, idk why. Any advice?
EDIT1: My func that fetches the Pokemon data from my service class is this one:
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
DispatchQueue.main.async {
self.pokemon? = poke
guard let skills = poke.abilities else { return }
for skill in skills {
guard let ability = skill.ability else { return }
guard var names = ability.name!.capitalized as? String else { return }
self.pokemon?.skillName = names
handler(names)
}
}
}
}
EDIT2: InfoView class looks like:
class InfoView: UIView {
// MARK: - Properties
var delegate: InfoViewDelegate?
// This whole block assigns the attributes that will be shown at the InfoView pop-up
// It makes the positioning of every element possible
var pokemon: Pokemon? {
didSet {
guard let pokemon = self.pokemon else { return }
guard let type = pokemon.type else { return }
guard let defense = pokemon.defense else { return }
guard let attack = pokemon.attack else { return }
guard let id = pokemon.id else { return }
guard let height = pokemon.height else { return }
guard let weight = pokemon.weight else { return }
guard let data = pokemon.image else { return }
if id == pokemon.id {
imageView.image = UIImage(data: data)
}
nameLabel.text = pokemon.name?.capitalized
configureLabel(label: typeLabel, title: "Type", details: type)
configureLabel(label: pokedexIdLabel, title: "Pokedex Id", details: "\(id)")
configureLabel(label: heightLabel, title: "Height", details: "\(height)")
configureLabel(label: defenseLabel, title: "Defense", details: "\(defense)")
configureLabel(label: weightLabel, title: "Weight", details: "\(weight)")
configureLabel(label: attackLabel, title: "Base Attack", details: "\(attack)")
}
}
let skillLabel: UILabel = {
let label = UILabel()
return label
}()
let imageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
return iv
}()
. . .
}
infoView.configureLabel is this:
func configureLabel(label: UILabel, title: String, details: String) {
let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(title): ", attributes: [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: Colors.softRed!]))
attributedText.append(NSAttributedString(string: "\(details)", attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.gray]))
label.attributedText = attributedText
}
EDIT 3: Structures design
struct Pokemon: Codable {
var results: [Species]?
var abilities: [Ability]?
var id, attack, defense: Int?
var name, type: String?
...
}
struct Ability: Codable {
let ability: Species?
}
struct Species: Codable {
let name: String?
let url: String?
}
Jump to the Edit2 paragraph for the final answer!
Initial Answer:
I looks like you UI does not get updated after the controller fetches all the data.
Since all of you UI configuration code is inside the var pokemon / didSet, it's a good idea to extract it to a separate method.
private func updateView(with pokemon: Pokemon?, details: String?) {
guard let id = pokemon?.id, let data = pokemon?.image else { return }
navigationItem.title = pokemon?.name?.capitalized
infoLabel.text = pokemon?.description
infoView.pokemon = pokemon
if id == pokemon?.id {
imageView.image = UIImage(data: data)
infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: details ?? "")
}
}
and now you can easily call in the the didSet
var pokemon: Pokemon? {
didSet { updateView(with: pokemon, details: allNames) }
}
and fetchPokemons completion aswell
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
guard var skillName = names as? String else { return }
self.pokemon?.skillName = skillName
self.allNames = skillName
print(self.allNames)
DispatchQueue.main.async {
self.updateView(with: self.pokemon, details: self.allNames)
}
}
}
It's super important to do any UI setup on the main queue.
Edit:
The fetch function may be causing the problems! you are calling handler multiple times:
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
DispatchQueue.main.async {
self.pokemon? = poke
guard let skills = poke.abilities else { return }
let names = skills.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
handler(names)
}
}
}
Edit2:
After looking at your codebase there are a couple of things you need to change:
1. fetchPokemons implementation
the handler of controller.service.fetchPokes gets called for every pokemon so we need to check if the fetched one is the current (self.pokemon) and then call the handler with properly formated skills.
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
guard poke.id == self.pokemon?.id else { return }
self.pokemon? = poke
let names = poke.abilities?.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
handler(names ?? "-")
}
}
2. update viewDidLoad()
now simply pass the names value to the label.
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
self.pokemon?.skillName = names
self.infoView.configureLabel(label: self.infoView.skillLabel, title: "Skills", details: names)
}
}
3. Refactor var pokemon: Pokemon? didSet observer
var pokemon: Pokemon? {
didSet {
guard let pokemon = pokemon, let data = pokemon.image else { return }
navigationItem.title = pokemon.name?.capitalized
infoLabel.text = pokemon.description!
infoView.pokemon = pokemon
imageView.image = UIImage(data: data)
}
}

SwiftUI Drag and Drop files

I am trying to add a "Drag and Drop" gesture / function to my SwiftUI Mac application.
I want to drop files from my System/ Desktop into my Application. It is possbile in regular Swift, which I found. I am trying to do this in SwiftUI now.
I find a onDrop() function in SwiftUI for Views. However, it looks like that this is only for internal gestures inside my application. I want to drag files from outside.
In Swift you need to register your NSView, for dragged Types.
registerForDraggedTypes([kUTTypeFileURL,kUTTypeImage])
I thought of creating a NSViewRepresentable and wrap that into my SwiftUI view.
This is the code I came up with, however I can not call registerForDraggedTyped.
final class DragDropView: NSViewRepresentable {
func makeNSView(context: NSViewRepresentableContext<DragDropView>) -> NSView {
let view = NSView()
view.registerForDraggedTypes([NSPasteboard.PasteboardType.pdf, NSPasteboard.PasteboardType.png])
return view
}
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<DragDropView>) {
}
Is there a simpler solution for that in SwiftUI? I would love to use that onDrop() function, but this is not working for external files, is it?
Here is a demo of drag & drop, tested with Xcode 11.4 / macOS 10.15.4.
Initial image is located on assets library, accepts drop (for simplicity only) as file url from Finder/Desktop (drop) and to TextEdit (drag), registers drag for TIFF representation.
struct TestImageDragDrop: View {
#State var image = NSImage(named: "image")
#State private var dragOver = false
var body: some View {
Image(nsImage: image ?? NSImage())
.onDrop(of: ["public.file-url"], isTargeted: $dragOver) { providers -> Bool in
providers.first?.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)
DispatchQueue.main.async {
self.image = image
}
}
})
return true
}
.onDrag {
let data = self.image?.tiffRepresentation
let provider = NSItemProvider(item: data as NSSecureCoding?, typeIdentifier: kUTTypeTIFF as String)
provider.previewImageHandler = { (handler, _, _) -> Void in
handler?(data as NSSecureCoding?, nil)
}
return provider
}
.border(dragOver ? Color.red : Color.clear)
}
}
As OP noted, this is only for a Mac OS application.
If you want to
open from Finder
drag from Finder
AND drag from elsewhere (like a website or iMessage)
then try this (drag image from this view not included).
XCode 11+ / SwiftUI 1.0+ / Swift 5:
Required Extension for opening from Finder:
extension NSOpenPanel {
static func openImage(completion: #escaping (_ result: Result<NSImage, Error>) -> ()) {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowedFileTypes = ["jpg", "jpeg", "png", "heic"]
panel.canChooseFiles = true
panel.begin { (result) in
if result == .OK,
let url = panel.urls.first,
let image = NSImage(contentsOf: url) {
completion(.success(image))
} else {
completion(.failure(
NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to get file location"])
))
}
}
}
}
The SwiftUI Views
struct InputView: View {
#Binding var image: NSImage?
var body: some View {
VStack(spacing: 16) {
HStack {
Text("Input Image (PNG,JPG,JPEG,HEIC)")
Button(action: selectFile) {
Text("From Finder")
}
}
InputImageView(image: self.$image)
}
}
private func selectFile() {
NSOpenPanel.openImage { (result) in
if case let .success(image) = result {
self.image = image
}
}
}
}
struct InputImageView: View {
#Binding var image: NSImage?
var body: some View {
ZStack {
if self.image != nil {
Image(nsImage: self.image!)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
Text("Drag and drop image file")
.frame(width: 320)
}
}
.frame(height: 320)
.background(Color.black.opacity(0.5))
.cornerRadius(8)
.onDrop(of: ["public.url","public.file-url"], isTargeted: nil) { (items) -> Bool in
if let item = items.first {
if let identifier = item.registeredTypeIdentifiers.first {
print("onDrop with identifier = \(identifier)")
if identifier == "public.url" || identifier == "public.file-url" {
item.loadItem(forTypeIdentifier: identifier, options: nil) { (urlData, error) in
DispatchQueue.main.async {
if let urlData = urlData as? Data {
let urll = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
if let img = NSImage(contentsOf: urll) {
self.image = img
print("got it")
}
}
}
}
}
}
return true
} else { print("item not here") return false }
}
}
}
Note: Have not needed to use the "public.image" identifier.
Optional Extensions if you need the result as PNG data (I did to upload to Firebase Storage):
extension NSBitmapImageRep {
var png: Data? { representation(using: .png, properties: [.compressionFactor:0.05]) }
}
extension Data {
var bitmap: NSBitmapImageRep? { NSBitmapImageRep(data: self) }
}
extension NSImage {
var png: Data? { tiffRepresentation?.bitmap?.png }
}
// usage
let image = NSImage(...)
if let data = image.png {
// do something further with the data
}
Can’t you use onDrop(of:isTargeted:perform:)? You can pass your array of supported types in the of argument.

SwiftUI: Send email

In a normal UIViewController in Swift, I use this code to send a mail.
let mailComposeViewController = configuredMailComposeViewController()
mailComposeViewController.navigationItem.leftBarButtonItem?.style = .plain
mailComposeViewController.navigationItem.rightBarButtonItem?.style = .plain
mailComposeViewController.navigationBar.tintColor = UIColor.white
if MFMailComposeViewController.canSendMail() {
self.present(mailComposeViewController, animated: true, completion: nil)
} else {
self.showSendMailErrorAlert()
}
How can I achieve the same in SwiftUI?
Do I need to use UIViewControllerRepresentable?
#Matteo's answer is good but it needs to use the presentation environment variable. I have updated it here and it addresses all of the concerns in the comments.
import SwiftUI
import UIKit
import MessageUI
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
Usage:
import SwiftUI
import MessageUI
struct ContentView: View {
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
var body: some View {
Button(action: {
self.isShowingMailView.toggle()
}) {
Text("Tap Me")
}
.disabled(!MFMailComposeViewController.canSendMail())
.sheet(isPresented: $isShowingMailView) {
MailView(result: self.$result)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As you mentioned, you need to port the component to SwiftUI via UIViewControllerRepresentable.
Here's a simple implementation:
struct MailView: UIViewControllerRepresentable {
#Binding var isShowing: Bool
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var isShowing: Bool
#Binding var result: Result<MFMailComposeResult, Error>?
init(isShowing: Binding<Bool>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_isShowing = isShowing
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
isShowing = false
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(isShowing: $isShowing,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
Usage:
struct ContentView: View {
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
var body: some View {
VStack {
if MFMailComposeViewController.canSendMail() {
Button("Show mail view") {
self.isShowingMailView.toggle()
}
} else {
Text("Can't send emails from this device")
}
if result != nil {
Text("Result: \(String(describing: result))")
.lineLimit(nil)
}
}
.sheet(isPresented: $isShowingMailView) {
MailView(isShowing: self.$isShowingMailView, result: self.$result)
}
}
}
(Tested on iPhone 7 Plus running iOS 13 - works like a charm)
Updated for Xcode 11.4
Answers are correct Hobbes the Tige & Matteo
From the comments, if you need to show an alert if no email is set up on the button or tap gesture
#State var isShowingMailView = false
#State var alertNoMail = false
#State var result: Result<MFMailComposeResult, Error>? = nil
HStack {
Image(systemName: "envelope.circle").imageScale(.large)
Text("Contact")
}.onTapGesture {
MFMailComposeViewController.canSendMail() ? self.isShowingMailView.toggle() : self.alertNoMail.toggle()
}
// .disabled(!MFMailComposeViewController.canSendMail())
.sheet(isPresented: $isShowingMailView) {
MailView(result: self.$result)
}
.alert(isPresented: self.$alertNoMail) {
Alert(title: Text("NO MAIL SETUP"))
}
To pre-populate To, Body ... also I add system sound same as Apple email sending sound
Parameters: recipients & messageBody can be injected when you init. MailView
import AVFoundation
import Foundation
import MessageUI
import SwiftUI
import UIKit
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
var recipients = [String]()
var messageBody = ""
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>)
{
_presentation = presentation
_result = result
}
func mailComposeController(_: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?)
{
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
if result == .sent {
AudioServicesPlayAlertSound(SystemSoundID(1001))
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.setToRecipients(recipients)
vc.setMessageBody(messageBody, isHTML: true)
vc.mailComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_: MFMailComposeViewController,
context _: UIViewControllerRepresentableContext<MailView>) {}
}
I also improved #Hobbes answer to easily configure parameters like, subject, recipients.
Checkout this gist
Even too lazy to checkout gist, then what about a SPM?
You can now easily copy paste this gift across different projects.
Usage;
import SwiftUI
import MessagesUI
// import SwiftUIEKtensions // via SPM
#State private var result: Result<MFMailComposeResult, Error>? = nil
#State private var isShowingMailView = false
var body: some View {
Form {
Button(action: {
if MFMailComposeViewController.canSendMail() {
self.isShowingMailView.toggle()
} else {
print("Can't send emails from this device")
}
if result != nil {
print("Result: \(String(describing: result))")
}
}) {
HStack {
Image(systemName: "envelope")
Text("Contact Us")
}
}
// .disabled(!MFMailComposeViewController.canSendMail())
}
.sheet(isPresented: $isShowingMailView) {
MailView(result: $result) { composer in
composer.setSubject("Secret")
composer.setToRecipients(["fancy#mail.com"])
}
}
}
Well, I have an old code that I used in SwiftUI in this way. The static function belongs to this class basically stays in my Utilities.swift file. But for demonstration purposes, I moved that in here.
Also to retain the delegate and works correctly, I have used this one as a singleton pattern.
Step 1: Create an Email Helper class
import Foundation
import MessageUI
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
public static let shared = EmailHelper()
private override init() {
//
}
func sendEmail(subject:String, body:String, to:String){
if !MFMailComposeViewController.canSendMail() {
// Utilities.showErrorBanner(title: "No mail account found", subtitle: "Please setup a mail account")
return //EXIT
}
let picker = MFMailComposeViewController()
picker.setSubject(subject)
picker.setMessageBody(body, isHTML: true)
picker.setToRecipients([to])
picker.mailComposeDelegate = self
EmailHelper.getRootViewController()?.present(picker, animated: true, completion: nil)
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
EmailHelper.getRootViewController()?.dismiss(animated: true, completion: nil)
}
static func getRootViewController() -> UIViewController? {
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController
// OR If you use SwiftUI 2.0 based WindowGroup try this one
// UIApplication.shared.windows.first?.rootViewController
}
}
Step 2: Just call this way in SwiftUI class
Button(action: {
EmailHelper.shared.sendEmail(subject: "Anything...", body: "", to: "")
}) {
Text("Send Email")
}
I am using this is in my SwiftUI based project.
Yeeee #Hobbes the Tige answer is good but...
Let's make it even better! What if user doesn't have Mail app (like I don't). You can handle it by trying out other mail apps.
if MFMailComposeViewController.canSendMail() {
self.showMailView.toggle()
} else if let emailUrl = Utils.createEmailUrl(subject: "Yo, sup?", body: "hot dog") {
UIApplication.shared.open(emailUrl)
} else {
self.alertNoMail.toggle()
}
createEmailUrl
static func createEmailUrl(subject: String, body: String) -> URL? {
let to = YOUR_EMAIL
let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
let gmailUrl = URL(string: "googlegmail://co?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let outlookUrl = URL(string: "ms-outlook://compose?to=\(to)&subject=\(subjectEncoded)")
let yahooMail = URL(string: "ymail://mail/compose?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let sparkUrl = URL(string: "readdle-spark://compose?recipient=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let defaultUrl = URL(string: "mailto:\(to)?subject=\(subjectEncoded)&body=\(bodyEncoded)")
if let gmailUrl = gmailUrl, UIApplication.shared.canOpenURL(gmailUrl) {
return gmailUrl
} else if let outlookUrl = outlookUrl, UIApplication.shared.canOpenURL(outlookUrl) {
return outlookUrl
} else if let yahooMail = yahooMail, UIApplication.shared.canOpenURL(yahooMail) {
return yahooMail
} else if let sparkUrl = sparkUrl, UIApplication.shared.canOpenURL(sparkUrl) {
return sparkUrl
}
return defaultUrl
}
Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlegmail</string>
<string>ms-outlook</string>
<string>readdle-spark</string>
<string>ymail</string>
</array>
I upgraded and simplified #Mahmud Assan's answer for the new SwiftUI Lifecycle.
import Foundation
import MessageUI
class EmailService: NSObject, MFMailComposeViewControllerDelegate {
public static let shared = EmailService()
func sendEmail(subject:String, body:String, to:String, completion: #escaping (Bool) -> Void){
if MFMailComposeViewController.canSendMail(){
let picker = MFMailComposeViewController()
picker.setSubject(subject)
picker.setMessageBody(body, isHTML: true)
picker.setToRecipients([to])
picker.mailComposeDelegate = self
UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true, completion: nil)
}
completion(MFMailComposeViewController.canSendMail())
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}
Usage:
Button(action: {
EmailService.shared.sendEmail(subject: "hello", body: "this is body", to: "asd#gmail.com") { (isWorked) in
if !isWorked{ //if mail couldn't be presented
// do action
}
}
}, label: {
Text("Send Email")
})
For anyone like me, wanting a better solution without glitching the screen of the user, i founded a very nice solution in this post from Medium.
The solution is similar to #Mahmud Assan's answer, but with more email app options and app alert with error.
I replaced some code for a method to allow the opening of more email apps, not only Mail or gmail.
First, remember to add the respective info in Info.plist, in my case:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlegmail</string>
<string>ms-outlook</string>
<string>readdle-spark</string>
<string>ymail</string>
</array>
After that you need to create a new swift file with the following code:
import SwiftUI
import MessageUI
class EmailHelper: NSObject {
/// singleton
static let shared = EmailHelper()
private override init() {}
}
extension EmailHelper {
func send(subject: String, body: String, to: [String]) {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
guard let viewController = windowScene?.windows.first?.rootViewController else {
return
}
if !MFMailComposeViewController.canSendMail() {
let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let mails = to.joined(separator: ",")
let alert = UIAlertController(title: "Cannot open Mail!", message: "", preferredStyle: .actionSheet)
var haveExternalMailbox = false
if let url = createEmailUrl(to: mails, subject: subjectEncoded, body: bodyEncoded), UIApplication.shared.canOpenURL(url) {
haveExternalMailbox = true
alert.addAction(UIAlertAction(title: "Gmail", style: .default, handler: { (action) in
UIApplication.shared.open(url)
}))
}
if haveExternalMailbox {
alert.message = "Would you like to open an external mailbox?"
} else {
alert.message = "Please add your mail to Settings before using the mail service."
if let settingsUrl = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(settingsUrl) {
alert.addAction(UIAlertAction(title: "Open Settings App", style: .default, handler: { (action) in
UIApplication.shared.open(settingsUrl)
}))
}
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
viewController.present(alert, animated: true, completion: nil)
return
}
let mailCompose = MFMailComposeViewController()
mailCompose.setSubject(subject)
mailCompose.setMessageBody(body, isHTML: false)
mailCompose.setToRecipients(to)
mailCompose.mailComposeDelegate = self
viewController.present(mailCompose, animated: true, completion: nil)
}
private func createEmailUrl(to: String, subject: String, body: String) -> URL? {
let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let gmailUrl = URL(string: "googlegmail://co?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let outlookUrl = URL(string: "ms-outlook://compose?to=\(to)&subject=\(subjectEncoded)")
let yahooMail = URL(string: "ymail://mail/compose?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let sparkUrl = URL(string: "readdle-spark://compose?recipient=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
let defaultUrl = URL(string: "mailto:\(to)?subject=\(subjectEncoded)&body=\(bodyEncoded)")
if let gmailUrl = gmailUrl, UIApplication.shared.canOpenURL(gmailUrl) {
return gmailUrl
} else if let outlookUrl = outlookUrl, UIApplication.shared.canOpenURL(outlookUrl) {
return outlookUrl
} else if let yahooMail = yahooMail, UIApplication.shared.canOpenURL(yahooMail) {
return yahooMail
} else if let sparkUrl = sparkUrl, UIApplication.shared.canOpenURL(sparkUrl) {
return sparkUrl
}
return defaultUrl
}
}
// MARK: - MFMailComposeViewControllerDelegate
extension EmailHelper: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}
Now, go to the view where you want to implement this:
struct OpenMailView: View {
var body: some View {
Button("Send email") {
EmailHelper.shared.send(subject: "Help", body: "", to: ["email#gmail.com"])
}
}
}
I've created a github repository for it. just add it to your project and use it like this:
struct ContentView: View {
#State var showMailSheet = false
var body: some View {
NavigationView {
Button(action: {
self.showMailSheet.toggle()
}) {
Text("compose")
}
}
.sheet(isPresented: self.$showMailSheet) {
MailView(isShowing: self.$showMailSheet,
resultHandler: {
value in
switch value {
case .success(let result):
switch result {
case .cancelled:
print("cancelled")
case .failed:
print("failed")
case .saved:
print("saved")
default:
print("sent")
}
case .failure(let error):
print("error: \(error.localizedDescription)")
}
},
subject: "test Subjet",
toRecipients: ["recipient#test.com"],
ccRecipients: ["cc#test.com"],
bccRecipients: ["bcc#test.com"],
messageBody: "works like a charm!",
isHtml: false)
.safe()
}
}
}
safe() modifier checks if MFMailComposeViewController.canSendMail() is false, it automatically dismesses the modal and tries to open a mailto link.
Before iOS 14, the default email app on iOS was Mail. Of course, you could have had other email apps installed
if MFMailComposeViewController.canSendMail() {
let mailController = MFMailComposeViewController(rootViewController: self)
mailController.setSubject("Test")
mailController.setToRecipients(["mail#test.com"])
mailController.mailComposeDelegate = self
present(mailController, animated: true, completion: nil)
}
Today
As a developer, I want to respect the user’s choice of email app, whether it’s Mail, Edison, Gmail, Outlook, or Hey. To do that, I can’t use MFMailComposeViewController. Instead, I have to add mailto to the LSApplicationQueriesSchemes key in Info.plist and then, when the user wants to send an email, use this code:
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [.universalLinksOnly : false]) { (success) in
// Handle success/failure
}
}
Unlike MFMailComposeViewController, this approach sends the user to their choice of email app and, at the same time, closes the source app. It’s not ideal.
I don't see the need of binding the isPresented or the result so my proposed solution is to use a callback when the MFMailComposeViewControllerDelegate is called. This also makes the result not nullable.
import Foundation
import MessageUI
import SwiftUI
import UIKit
public struct MailView: UIViewControllerRepresentable {
public struct Attachment {
public let data: Data
public let mimeType: String
public let filename: String
public init(data: Data, mimeType: String, filename: String) {
self.data = data
self.mimeType = mimeType
self.filename = filename
}
}
public let onResult: ((Result<MFMailComposeResult, Error>) -> Void)
public let subject: String?
public let message: String?
public let attachment: Attachment?
public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
public var onResult: ((Result<MFMailComposeResult, Error>) -> Void)
init(onResult: #escaping ((Result<MFMailComposeResult, Error>) -> Void)) {
self.onResult = onResult
}
public func mailComposeController(
_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?
) {
if let error = error {
self.onResult(.failure(error))
} else {
self.onResult(.success(result))
}
}
}
public init(
subject: String? = nil,
message: String? = nil,
attachment: MailView.Attachment? = nil,
onResult: #escaping ((Result<MFMailComposeResult, Error>) -> Void)
) {
self.subject = subject
self.message = message
self.attachment = attachment
self.onResult = onResult
}
public func makeCoordinator() -> Coordinator {
Coordinator(onResult: onResult)
}
public func makeUIViewController(
context: UIViewControllerRepresentableContext<MailView>
) -> MFMailComposeViewController {
let controller = MFMailComposeViewController()
controller.mailComposeDelegate = context.coordinator
if let subject = subject {
controller.setSubject(subject)
}
if let message = message {
controller.setMessageBody(message, isHTML: false)
}
if let attachment = attachment {
controller.addAttachmentData(
attachment.data,
mimeType: attachment.mimeType,
fileName: attachment.filename
)
}
return controller
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>
) {
// nothing to do here
}
}
Usage
struct ContentView: View {
#State var showEmailComposer = false
var body: some View {
Button("Tap me") {
showEmailComposer = true
}
.sheet(isPresented: $showEmailComposer) {
MailView(
subject: "Email subject",
message: "Message",
attachment: nil,
onResult: { _ in
// Handle the result if needed.
self.showEmailComposer = false
}
)
}
}
}
Unfortunately, #Matteo's solution doesn't work perfectly for me. It looks buggy :(
Alternative solution
struct MailComposeSheet<T: View>: UIViewControllerRepresentable {
let view: T
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
uiViewController.rootView = view
if isPresented, uiViewController.presentedViewController == nil {
let picker = MFMailComposeViewController()
picker.mailComposeDelegate = context.coordinator
picker.presentationController?.delegate = context.coordinator
uiViewController.present(picker, animated: true)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MFMailComposeViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
var parent: MailComposeSheet
init(_ mailComposeSheet: MailComposeSheet) {
self.parent = mailComposeSheet
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) { [weak self] in
self?.parent.isPresented = false
}
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
parent.isPresented = false
}
}
}
extension View {
func mailComposeSheet(isPresented: Binding<Bool>) -> some View {
MailComposeSheet(
view: self,
isPresented: isPresented
)
}
}
Usage:
struct ContentView: View {
#State var showEmailComposer = false
var body: some View {
Button("Tap me") {
showEmailComposer = true
}
.mailComposeSheet(isPresented: $showEmailComposer)
}
}
I'm new to Swift, please tell me if I'm doing something wrong.
I went through all the answers above - continued to get AXSERVER / CPT port errors.
What worked for me
Button(action: {
let email = "mailto://"
let emailformatted = email + centreStaff.userName // from MongoDB Atlas
guard let url = URL(string: emailformatted) else { return }
UIApplication.shared.open(url)
}) {
Image (systemName: "envelope.circle.fill")
.symbolRenderingMode(.multicolor)
}
opens Outlook with name of the staff filled in...and Boom! email sent.