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)
}
}
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.
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.