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.
Related
How to return from the ASWebAutheticationSession completion handler back to the View?
Edit: Just for clearance this is not the original code in my project this is extremely shortened and is just for showcasing what I mean.
Here's an example of a code
struct SignInView: View {
#EnvironmentObject var signedIn: UIState
var body: some View {
let AuthenticationSession = AuthSession()
AuthenticationSession.webAuthSession.presentationContextProvider = AuthenticationSession
AuthenticationSession.webAuthSession.prefersEphemeralWebBrowserSession = true
AuthenticationSession.webAuthSession.start()
}
}
class AuthSession: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {
var webAuthSession = ASWebAuthenticationSession.init(
url: AuthHandler.shared.signInURL()!,
callbackURLScheme: "",
completionHandler: { (callbackURL: URL?, error: Error?) in
// check if any errors appeared
// get code from authentication
// Return to view to move on with code? (return code)
})
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return ASPresentationAnchor()
}
}
So what I'm trying to do is call the sign In process and then get back to the view with the code from the authentication to move on with it.
Could somebody tell me how this may be possible?
Thanks.
Not sure if I'm correctly understanding your question but it is normally done with publishers, commonly with the #Published wrapper, an example:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Button {
self.viewModel.signIn(user: "example", password: "example")
}
label: {
Text("Sign in")
}
if self.viewModel.signedIn {
Text("Successfully logged in")
}
else if let error = self.viewModel.signingError {
Text("Error while logging in: \(error.localizedDescription)")
}
}
.padding()
}
}
class ViewModel: ObservableObject {
#Published var signingStatus = SigningStatus.idle
var signedIn: Bool {
if case .success = self.signingStatus { return true }
return false
}
var signingError: Error? {
if case .failure(let error) = self.signingStatus { return error }
return nil
}
func signIn(user: String, password: String) {
self.dummyAsyncProcessWithCompletionHandler { [weak self] success in
guard let self = self else { return }
guard success else {
self.updateSigning(.failure(CustomError(errorDescription: "Login failed")))
return
}
self.updateSigning(.success)
}
}
private func updateSigning(_ status: SigningStatus) {
DispatchQueue.main.async {
self.signingStatus = status
}
}
private func dummyAsyncProcessWithCompletionHandler(completion: #escaping (Bool) -> ()) {
Task {
print("Signing in")
try await Task.sleep(nanoseconds: 500_000_000)
guard Int.random(in: 0..<9) % 2 == 0 else {
print("Error")
completion(false)
return
}
print("Success")
completion(true)
}
}
enum SigningStatus {
case idle
case success
case failure(Error)
}
struct CustomError: Error, LocalizedError {
var errorDescription: String?
}
}
im fairly new to swiftUI... pardon the ignorance :) I have most of the code entered in my project from the instructions on the Firebase website. For some reason apple authentication will not successfully authenticate. No idea why, I suspect the nonce part of the code located in 'ConentView' is potentially not being linked to the service. If anyone has any thoughts on why this is occurring I would be greatly appreciative for any help?
Xcode simulator - will not load past this screen
ContentView:
import SwiftUI
import FirebaseAuthUI
import CryptoKit
import AuthenticationServices
struct ContentView: View {
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
String(format: "%02x", $0)
}.joined()
return hashString
}
#ObservedObject private var authStateManager =
FirebaseAuthStateManager()
#State var isShowSheet = false
#State var currentNonce:String?
private func randomNonceString(length: Int = 32) ->
String {
precondition(length > 0)
let charset: [Character] =
***key***
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode =
SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError(
"Unable to generate nonce. SecRandomCopyBytes
failed with OSStatus \(errorCode)"
)
}
return random
}
randoms.forEach { random in
if remainingLength == 0 {
return
}
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
func authorizationController(controller:
ASAuthorizationController, didCompleteWithAuthorization
authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential
as? ASAuthorizationAppleIDCredential {
guard let nonce = currentNonce else {
fatalError("Invalid state: A login callback was
received, but no login request was sent.")
}
guard let appleIDToken =
appleIDCredential.identityToken else {
print("Unable to fetch identity token")
return
}
guard let idTokenString = String(data: appleIDToken,
encoding:
.utf8) else {
print("Unable to serialize token string from data: \
(appleIDToken.debugDescription)")
return
}
// Initialize a Firebase credential.
let credential =
OAuthProvider.credential(withProviderID:"apple.com",
idToken: idTokenString,rawNonce: nonce)
// Sign in with Firebase.
Auth.auth().signIn(with: credential) { (authResult,
error) in
if (error != nil) {
// Error. If error.code ==
.MissingOrInvalidNonce, make sure
// you're sending the SHA256-hashed nonce as a
hex string with
// your request to Apple.
print(error?.localizedDescription as Any)
return
}
// User is signed in to Firebase with Apple.
// ...
}
}
func authorizationController(controller:
ASAuthorizationController, didCompleteWithError error:
Error) {
// Handle error.
print("Sign in with Apple errored: \(error)")
}
}
var body: some View {
VStack {
if authStateManager.signInState == false {
// Sign-Out
Button(action: {
self.isShowSheet.toggle()
}) {
Text("Sign-In")
}
} else {
// Sign-In
Button(action: {
do {
try Auth.auth().signOut()
} catch {
print("Error")
}
}) {
Text("Sign-Out")
}
}
}
.sheet(isPresented: $isShowSheet) {
FirebaseUIView(isShowSheet: self.$isShowSheet)
}
}
}
FirebaseUIView:
import SwiftUI
import FirebaseAuthUI
import FirebaseGoogleAuthUI
import FirebaseOAuthUI
import CryptoKit
import AuthenticationServices
import FirebaseEmailAuthUI
struct FirebaseUIView: UIViewControllerRepresentable {
#Binding var isShowSheet: Bool
class Coordinator: NSObject,
FUIAuthDelegate {
// FirebaseUIView
let parent: FirebaseUIView
//
init(_ parent: FirebaseUIView) {
self.parent = parent
}
// MARK: - FUIAuthDelegate
func authUI(_ authUI: FUIAuth, didSignInWith user:
User?, error: Error?) {
// handle user and error as necessary
if let error = error {
//
print("Auth NG:\
(error.localizedDescription)")
}
if let _ = user {
//
}
// Sheet(ModalView)
parent.isShowSheet = false
}
}
func makeCoordinator() -> Coordinator {
// Coordinator
Coordinator(self)
}
func makeUIViewController(context: Context) ->
UINavigationController {
let authUI = FUIAuth.defaultAuthUI()!
// You need to adopt a FUIAuthDelegate protocol to
receive callback
authUI.delegate = context.coordinator
let providers: [FUIAuthProvider] = [
FUIGoogleAuth(authUI: authUI),
FUIOAuth.microsoftAuthProvider(),
// FUIFacebookAuth(authUI: authUI),
// FUIOAuth.twitterAuthProvider(),
FUIEmailAuth(),
// FUIPhoneAuth(authUI:authUI),
FUIOAuth.appleAuthProvider(),
]
authUI.providers = providers
// FirebaseUI
let authViewController = authUI.authViewController()
return authViewController
}
func updateUIViewController(_ uiViewController:
UINavigationController, context: Context) {
}
}
[ContentView[][2]2
Check out SignInWithApple from FirebaseService: https://github.com/rebeloper/FirebaseService
Turns out there was nothing wrong with my code. The moment I switched from simulator to the actual iPhone device the code worked perfectly. This is a known BUG with the simulator in Xcode.
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()
The user looks for the delivery address on the map, then the address is identified by the marker located in the middle of the screen. And then the address is obtained through this marker. How to display an address in the user interface ?
struct MapView: UIViewRepresentable {
#Binding var centerCoordinate: CLLocationCoordinate2D
var currentLocation: CLLocationCoordinate2D?
var withAnnotation: MKPointAnnotation?
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
var addressLabel: String = "222"
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if !mapView.showsUserLocation {
parent.centerCoordinate = mapView.centerCoordinate
}
}
...
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
let center = getCenterLocation(for: mapView)
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(center) { [weak self] (placemarks, error) in
guard let self = self else { return }
if let _ = error {
//TODO: Show alert informing the user
print("error")
return
}
guard let placemark = placemarks?.first else {
//TODO: Show alert informing the user
return
}
let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""
DispatchQueue.main.async {
self.addressLabel = String("\(streetName) | \(streetNumber)")
print(self.addressLabel)
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = false
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if let currentLocation = self.currentLocation {
if let annotation = self.withAnnotation {
uiView.removeAnnotation(annotation)
}
uiView.showsUserLocation = true
let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
uiView.setRegion(region, animated: true)
} else if let annotation = self.withAnnotation {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotation(annotation)
}
}
}
I am trying to pass the address to the UI.
What's the most correct way to do this?
In the interface, I want to get the address from an ever-changing variable addressLabel
import SwiftUI
import MapKit
fileprivate let locationFetcher = LocationFetcher()
struct LocationView: View {
#State var centerCoordinate = CLLocationCoordinate2D()
#State var currentLocation: CLLocationCoordinate2D?
#State var annotation: MKPointAnnotation?
var body: some View {
ZStack {
MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation)
.edgesIgnoringSafeArea([.leading, .trailing, .bottom])
.onAppear(perform: {
locationFetcher.start()
})
}
.overlay(
ZStack {
Text("\(*MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation).makeCoordinator().addressLabel OMG????*)")
.offset(y: 44)
}
)
}
struct LocationView_Previews: PreviewProvider {
static var previews: some View {
LocationView()
}
}
How can i do this ?
Thanks in advance
Here is one approach. Have a single source of truth that both UIKit and SwiftUI can access.
#available(iOS 15.0, *)
struct LocationView: View {
//It is better to have one source of truth
#StateObject var vm: MapViewModel = MapViewModel()
var body: some View {
ZStack {
MapView(vm: vm)
.edgesIgnoringSafeArea([.leading, .trailing, .bottom])
.onAppear(perform: {
//locationFetcher.start() //No Code provided
})
}
.overlay(
HStack{
Spacer()
Text(vm.addressLabel)
Spacer()
//Using offset is subjective since screen sizes change just center it
}
)
//Sample alert that adapts to what is
.alert(isPresented: $vm.errorAlert.isPresented, error: vm.errorAlert.error, actions: {
if vm.errorAlert.defaultAction != nil{
Button("ok", role: .none, action: vm.errorAlert.defaultAction!)
}
if vm.errorAlert.cancelAction != nil{
Button("cancel", role: .cancel, action: vm.errorAlert.cancelAction!)
}
if vm.errorAlert.defaultAction == nil && vm.errorAlert.cancelAction == nil {
Button("ok", role: .none, action: {})
}
})
}
}
//UIKit and SwiftUI will have access to this ViewModel so all the data can have one souce of truth
class MapViewModel: ObservableObject{
//All the variables live here
#Published var addressLabel: String = "222"
#Published var centerCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D()
#Published var currentLocation: CLLocationCoordinate2D? = nil
#Published var withAnnotation: MKPointAnnotation? = nil
#Published var annotation: MKPointAnnotation?
//This tuple variable allows you to have a dynamic alert in the view
#Published var errorAlert: (isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?) = (false, MapErrors.unknown, nil, nil)
//The new alert requires a LocalizedError
enum MapErrors: LocalizedStringKey, LocalizedError{
case unknown
case failedToRetrievePlacemark
case failedToReverseGeocode
case randomForTestPurposes
//Add localizable.strings to you project and add these keys so you get localized messages
var errorDescription: String?{
switch self{
case .unknown:
return "unknown".localizedCapitalized
case .failedToRetrievePlacemark:
return "failedToRetrievePlacemark".localizedCapitalized
case .failedToReverseGeocode:
return "failedToReverseGeocode".localizedCapitalized
case .randomForTestPurposes:
return "randomForTestPurposes".localizedCapitalized
}
}
}
//Presenting with this will ensure that errors keep from gettting lost by creating a loop until they can be presented
func presentError(isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?, count: Int = 1){
//If there is an alert already showing
if errorAlert.isPresented{
//See if the current error has been on screen for 10 seconds
if count >= 10{
//If it has dismiss it so the new error can be posted
errorAlert.isPresented = false
}
//Call the method again in 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let newCount = count + 1
self.presentError(isPresented: isPresented, error: error, defaultAction: defaultAction, cancelAction: cancelAction, count: newCount)
}
}else{
errorAlert = (isPresented, error, defaultAction, cancelAction)
}
}
}
struct MapView: UIViewRepresentable {
#ObservedObject var vm: MapViewModel
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if !mapView.showsUserLocation {
parent.vm.centerCoordinate = mapView.centerCoordinate
}
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
getAddress(center: mapView.centerCoordinate)
//Just to demostrate the error
//You can remove this whenever
#if DEBUG
if Bool.random(){
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.randomForTestPurposes, defaultAction: nil, cancelAction: nil)
}
#endif
}
//Gets the addess from CLGeocoder if available
func getAddress(center: CLLocationCoordinate2D){
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(CLLocation(latitude: center.latitude, longitude: center.longitude)) { [weak self] (placemarks, error) in
guard let self = self else { return }
if let _ = error {
//TODO: Show alert informing the user
print("error")
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToReverseGeocode, defaultAction: nil, cancelAction: nil)
return
}
guard let placemark = placemarks?.first else {
//TODO: Show alert informing the user
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToRetrievePlacemark, defaultAction: nil, cancelAction: nil)
return
}
let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""
DispatchQueue.main.async {
self.parent.vm.addressLabel = String("\(streetName) | \(streetNumber)")
print(self.parent.vm.addressLabel)
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = false
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if let currentLocation = vm.currentLocation {
if let annotation = vm.withAnnotation {
uiView.removeAnnotation(annotation)
}
uiView.showsUserLocation = true
let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
uiView.setRegion(region, animated: true)
} else if let annotation = vm.withAnnotation {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotation(annotation)
}
}
}
I'm new to iOS dev, so sorry if it's an obvious question. But I can't figure out how to update the data in SwiftUI List. I'm fetching the data from API and using #ObservedObject to pass it to the ContentView. It works fine when I'm launching my app, but after I change my API request (by typing a keyword in the SearchBar) and fetch it again, it doesn't seem to update the List, even though the data was changed.
ContentView.swift
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
#State var searchText: String = ""
var body: some View {
NavigationView{
VStack {
SearchBar(text: $searchText, placeholder: "Enter a keyword")
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(post.title)
}
}
}.gesture(DragGesture().onChanged { _ in UIApplication.shared.endEditing() })
}.navigationBarTitle("News")
}
.onAppear {
self.networkManager.fetchData(self.searchText)
}
}
}
NetworkManager.swift
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fetchData(_ keyword: String?){
var urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf"
if keyword != nil {
urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf&q=\(keyword!)"
}
print(urlString)
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data{
do{
let results = try decoder.decode(News.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.articles
print(self.posts)
}
} catch{
print(error)
}
}
}
}
task.resume()
}
}
}
SearchBar.swift (I fetch data again inside searchBarSearchButtonClicked)
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
class Coordinator: NSObject, UISearchBarDelegate {
#ObservedObject var networkManager = NetworkManager()
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
print(text)
DispatchQueue.main.async {
self.networkManager.fetchData(self.text)
}
UIApplication.shared.endEditing()
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
News.swift
struct News: Decodable {
let articles: [Post]
}
struct Post: Decodable, Identifiable {
var id: String{
return url!
}
let title: String
let url: String?
}
I've made a few minor modifications and made the code work in Xcode-playgrounds. Here's how:
Model:
struct News: Codable { var articles: [Post] }
struct Post: Identifiable, Codable { var title: String; var id: String { title } }
ContentView:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
VStack {
TextField("Enter a keyword", text: $networkManager.searchText)
List(networkManager.posts) { post in
NavigationLink(destination: EmptyView()) {
HStack {
Text(post.title)
}
}
}
}.navigationBarTitle("News")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
NetworkManager:
class NetworkManager: ObservableObject {
#Published var searchText: String = "" {
didSet {
fetchData()
}
}
#Published var posts = [Post]()
func fetchData() {
let urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf&q=\(searchText)"
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data{
do{
let results = try decoder.decode(News.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.articles
print(self.posts)
}
} catch{
print(error)
}
}
}
}
task.resume()
}
}
}