Failing to send email - ios

I have a button that allows you to send an email with all the content on your app.
I'm iterating thru all the data stored in a core data container, and creating a string that I then pass to the sheet presenting the ability to send email.
When I test it, the string always seems to be empty, and I can see an error: [PPT] Error creating the CFMessagePort needed to communicate with PPT.
I'm using the same mechanism I use to email each item on the list, which works like a charm.
Anyway, I've see a lot of posts about the error, but nothing that points to a solution.
Here's the code, maybe it's related to how I call the .sheet? What am I missing? What's that error even try to tell me?
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State private var sheetbackupJotMail = false
//for sending mail
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
#State private var emailText: String = ""
var body: some View {
NavigationView {
List (jots) { jot in
Text(jot.text!)
}
}
.toolbar {
// toolbar button that send the message with all content
ToolbarItem(placement: .navigation) {
if MFMailComposeViewController.canSendMail() {
Button(action: {
sheetbackupJotMail.toggle()
}) {
Label("Back up all jots", systemImage: "arrow.up.square").foregroundColor(.secondary)
}
// sheet for backing up email
.sheet(isPresented: $sheetbackupJotMail) {
MailView(result: $result) { composer in
emailText = ""
for jot in jots {
emailText = emailText + jot.dateText! + "\n" + jot.text! + "\n\n"
}
print(">>>: " + emailText) //<-- this is always empty, however if I move to the button before the toggle(), I get the right text
// emailing all
composer.setSubject("Jot Backup")
composer.setMessageBody(emailText, isHTML: false)
}
}
}
}
}
}
}
// mail view
import SwiftUI
import UIKit
import MessageUI
public struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
public var configure: ((MFMailComposeViewController) -> Void)?
public 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
}
public 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)
}
}
public func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
configure?(vc)
return vc
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
Also moving the creation of the text here caused the text I need to mail to be ok, but the error continues to be: PPT] Error creating the CFMessagePort needed to communicate with PPT.
Button(action: {
emailText = ""
for jot in jots {
emailText = emailText + jot.dateText! + "\n" + jot.text! + "\n\n"
}
print(">>>: " + emailText)
sheetbackupJotMail.toggle()
}) {
Label("Back up all jots", systemImage: "arrow.up.square").foregroundColor(.secondary)
}

The sheet content is computed only once on creation time, so all later changes in dependencies are not re-injected, so you have to put everything dependent by binding inside MailView and do composing there.
I.e. your sheet should look like (sketch)
.sheet(isPresented: $sheetbackupJotMail) {
MailView(result: $result, emailText: $emailText, jots: $jots)
}
*Many dependencies are missing, so it is possible to provide testable solution, but the idea should be clear. See also https://stackoverflow.com/a/64554083/12299030 - it demos solution for similar issue.

Related

Confirm Stripe payment in SwiftUI (iOS 16)

I am using Stripe API to accept card payment in SwiftUI.
I managed to make successful payment using STPPaymentCardTextField wrapped in UIViewRepresentable and confirm it using the provided sheet modifier .paymentConfirmationSheet(....).
The problem appeared when I adopted the new NavigationStack in iOS 16. It appears that the .paymentConfirmationSheet(....) doesn't work properly if it is presented inside the new NavigationStack.
Is there any other way I can confirm card payment in SwiftUI? How can I fix this?
If I switch back to NavigationView it works as expected but I would want to use the new features of NavigationStack.
My demo checkout view and its viewmodel
struct TestCheckout: View {
#ObservedObject var model = TestCheckoutViewModel()
var body: some View {
VStack {
CardField(paymentMethodParams: $model.paymentMethodParams)
Button("Pay") {
model.pay()
}
.buttonStyle(LargeButtonStyle())
.paymentConfirmationSheet(isConfirmingPayment: $model.confirmPayment,
paymentIntentParams: model.paymentIntentParams,
onCompletion: model.onPaymentComplete)
}
}
}
class TestCheckoutViewModel: ObservableObject {
#Published var paymentMethodParams: STPPaymentMethodParams?
#Published var confirmPayment = false
#Published var paymentIntentParams = STPPaymentIntentParams(clientSecret: "")
func pay() {
Task {
do {
// Create dummy payment intent
let paymentIntent = try await StripeManager.shared.getPaymentIntent(orderId: "", amount: 1000)
// Collect card details
let paymentIntentParams = STPPaymentIntentParams(clientSecret: paymentIntent.secret)
paymentIntentParams.paymentMethodParams = paymentMethodParams
// Submit the payment
DispatchQueue.main.async {
self.paymentIntentParams = paymentIntentParams
self.confirmPayment = true
}
} catch {
print(error)
}
}
}
func onPaymentComplete(status: STPPaymentHandlerActionStatus, paymentIntent: STPPaymentIntent?, error: Error?) {
print("Payment completed: \(error)")
}
}
Now this doesn't work
struct TestView: View {
var body: some View {
NavigationStack {
NavigationLink("Checkout", value: "checkout")
.navigationDestination(for: String.self) { string in
if string == "checkout" {
TestCheckout()
}
}
}
}
}
But this does
struct TestView: View {
var body: some View {
TestCheckout()
}
}
The error I get is:
Error Domain=com.stripe.lib Code=50 "There was an unexpected error -- try again in a few seconds" UserInfo={NSLocalizedDescription=There was an unexpected error -- try again in a few seconds, com.stripe.lib:StripeErrorTypeKey=invalid_request_error, com.stripe.lib:StripeErrorCodeKey=payment_intent_unexpected_state, com.stripe.lib:ErrorMessageKey=Nemůžete potvrdit tento Platební záměr, protože v něm chybí platební metoda. Můžete buď aktualizovat Platební záměr s platební metodou a pak jej znovu potvrdit, nebo jej znovu potvrďte přímo s platební metodou.}
Finally I found the problem.
The problem is the #ObservedObject var model = TestCheckoutViewModel().
For some reason it doesn't work with #ObservedObject anymore and it has to be #StateObject.

Unable to access variable values in view

Trying to send email from my iOS app. It have it set up and it's good to go, but I can't seem to be able to get the text passed to the view presented when sending the email. When I pass the text to be sent, it's always empty.
I know it might be related to the view not having access to it, but I'm scratching my head what to change, or what to add in order to make it work. I have tried with #binding and ObservableObject, but I'm still new with Swift and SwiftUI, so I'm making a mess.
Here's the code, how can I pass the text from the list item to the new view presented?
struct ContentView: View {
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
// added this to try to force the text to go, since passing jot.text was giving me always
// the first item in the list
#State private var emailText: String = ""
var body: some View {
NavigationView {
List(jots) { jot in
Text(jot.text!)
.contextMenu {
if MFMailComposeViewController.canSendMail() {
Button(action: {
emailText = jot.text! // try to force the text to be passed
self.isShowingMailView.toggle()
}) {
Text("Email jot")
Image(systemName: "envelope")
}
}
}
.sheet(isPresented: $isShowingMailView) {
MailView(result: $result) { composer in
composer.setSubject("Jot!")
// in here, if I pass jot.text! then it's always the first item in the list
// if I pass emailText then it's always empty
composer.setMessageBody(emailText, isHTML: false)
}
}
}
.listStyle(.plain)
}
}
}
And the supporting code to send email:
import SwiftUI
import UIKit
import MessageUI
public struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
public var configure: ((MFMailComposeViewController) -> Void)?
public 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
}
public 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)
}
}
public func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
configure?(vc)
return vc
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
We don't have a full Minimal Reproducible Example (MRE), but I think what you want is to use the sheet(item:onDismiss:content:) initializer. Instead of using a Bool to trigger the sheet showing, it triggers when an optional value of whatever data you wish to pass in becomes non-nil. This way, you can pass the data to the .sheet and only need one variable to do it. This is untested, but try:
struct ContentView: View {
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
// this is your optional selection variable
#State private var selectedJot: Jot?
var body: some View {
NavigationView {
List(jots) { jot in
Text(jot.text!)
.contextMenu {
if MFMailComposeViewController.canSendMail() {
Button(action: {
// this gives selectedJot a value making it non-nil
selectedJot = jot
}) {
Text("Email jot")
Image(systemName: "envelope")
}
}
}
}
.listStyle(.plain)
// When selectedJot becomes non-nil, this initializer will trigger the sheet.
.sheet(item: $selectedJot) { jot in
MailView(result: $result) { composer in
composer.setSubject("Jot!")
composer.setMessageBody(jot.text, isHTML: false)
}
}
}
}
}

UICloudSharingController Does not Display/Work with CloudKit App

I have been struggling to get an app to work with CloudKit and record sharing. I have
created several apps that sync Core Data records among devices for one user. I have not
been able to get the UICloudSharingController to work for sharing records. I can display
the Sharing View Controller, but tapping on Mail or Message displays a keyboard but no
address field and no way to dismiss the view. I have been so frustrated by it that I
decided to try the Apple "Sharing" sample app to start from the basics. However, the
sample app does not work for me either.
Here's the link to the sample app:
https://github.com/apple/cloudkit-sample-sharing/tree/swift-concurrency
The code below is pretty much straight from the sample app.
This is the ContentView file:
import SwiftUI
import CloudKit
struct ContentView: View {
#EnvironmentObject private var vm: ViewModel
#State private var isAddingContact = false
#State private var isSharing = false
#State private var isProcessingShare = false
#State private var showShareView = false
#State private var showIntermediateView = false
#State private var activeShare: CKShare?
#State private var activeContainer: CKContainer?
var body: some View {
NavigationView {
contentView
.navigationTitle("Contacts")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button { Task.init { try await vm.refresh() } } label: { Image(systemName: "arrow.clockwise") }
}
ToolbarItem(placement: .navigationBarLeading) {
progressView
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { isAddingContact = true }) { Image(systemName: "plus") }
}
}
}
.onAppear {
Task.init {
try await vm.initialize()
try await vm.refresh()
}
}
.sheet(isPresented: $isAddingContact, content: {
AddContactView(onAdd: addContact, onCancel: { isAddingContact = false })
})
}
/// This progress view will display when either the ViewModel is loading, or a share is processing.
var progressView: some View {
let showProgress: Bool = {
if case .loading = vm.state {
return true
} else if isProcessingShare {
return true
}
return false
}()
return Group {
if showProgress {
ProgressView()
}
}
}
/// Dynamic view built from ViewModel state.
private var contentView: some View {
Group {
switch vm.state {
case let .loaded(privateContacts, sharedContacts):
List {
Section(header: Text("Private")) {
ForEach(privateContacts) { contactRowView(for: $0) }
}
Section(header: Text("Shared")) {
ForEach(sharedContacts) { contactRowView(for: $0, shareable: false) }
}
}.listStyle(GroupedListStyle())
case .error(let error):
VStack {
Text("An error occurred: \(error.localizedDescription)").padding()
Spacer()
}
case .loading:
VStack { EmptyView() }
}
}
}
/// Builds a `CloudSharingView` with state after processing a share.
private func shareView() -> CloudSharingView? {
guard let share = activeShare, let container = activeContainer else {
return nil
}
return CloudSharingView(container: container, share: share)
}
/// Builds a Contact row view for display contact information in a List.
private func contactRowView(for contact: Contact, shareable: Bool = true) -> some View {
HStack {
VStack(alignment: .leading) {
Text(contact.name)
Text(contact.phoneNumber)
.textContentType(.telephoneNumber)
.font(.footnote)
}
if shareable {
Spacer()
Button(action: { Task.init { try? await shareContact(contact) }
}, label: { Image(systemName: "square.and.arrow.up") }).buttonStyle(BorderlessButtonStyle())
.sheet(isPresented: $isSharing, content: { shareView() })
}//if sharable
}//h
}//contact row view
// MARK: - Actions
private func addContact(name: String, phoneNumber: String) async throws {
try await vm.addContact(name: name, phoneNumber: phoneNumber)
try await vm.refresh()
isAddingContact = false
}
private func shareContact(_ contact: Contact) async throws {
isProcessingShare = true
do {
let (share, container) = try await vm.createShare(contact: contact)
isProcessingShare = false
activeShare = share
activeContainer = container
isSharing = true
} catch {
debugPrint("Error sharing contact record: \(error)")
}
}
}
And the UIViewControllerRepresentable file for the sharing view:
import Foundation
import SwiftUI
import UIKit
import CloudKit
/// This struct wraps a `UIImagePickerController` for use in SwiftUI.
struct CloudSharingView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
let container: CKContainer
let share: CKShare
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeUIViewController(context: Context) -> some UIViewController {
let sharingController = UICloudSharingController(share: share, container: container)
sharingController.availablePermissions = [.allowReadWrite, .allowPrivate]
sharingController.delegate = context.coordinator
return sharingController
}
func makeCoordinator() -> CloudSharingView.Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UICloudSharingControllerDelegate {
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
debugPrint("Error saving share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
"Sharing Example"
}
}
}
This is the presented screen when tapping to share a record. This all looks as expected:
This is the screen after tapping Messages (same result for Mail). I don't see any way to influence the second screen presentation - it seems that the view controller representable is not working with this version of Xcode:
Any guidance would be appreciated. Xcode 13.1, iOS 15 and I am on macOS Monterrey.
I had the same issue and fixed it by changing makeUIViewController() in CloudSharingView.swift:
func makeUIViewController(context: Context) -> some UIViewController {
share[CKShare.SystemFieldKey.title] = "Boom"
let sharingController = UICloudSharingController(share: share, container: container)
sharingController.availablePermissions = [.allowReadWrite, .allowPrivate]
sharingController.delegate = context.coordinator
-->>> sharingController.modalPresentationStyle = .none
return sharingController
}
It seems like some value work, some don't. Not sure why.

SwiftUI #State not triggering update when value changed by separate method

I have this (simpilied) section of code for a SwiftUI display:
struct ContentView: View {
private var errorMessage: String?
#State private var showErrors: Bool = false
var errorAlert: Alert {
Alert(title: Text("Error!"),
message: Text(errorMessage ?? "oops!"),
dismissButton: .default(Text("Ok")))
}
init() {}
var body: some View {
VStack {
Text("Hello, World!")
Button(action: {
self.showErrors.toggle()
}) {
Text("Do it!")
}
}
.alert(isPresented: $showErrors) { errorAlert }
}
mutating func display(errors: [String]) {
errorMessage = errors.joined(separator: "\n")
showErrors.toggle()
}
}
When the view is displayed and I tape the "Do it!" button then the alert is displayed as expected.
However if I call the display(errors:...) function the error message is set, but the display does not put up an alert.
I'm guessing this is something to do with the button being inside the view and the function being outside, but I'm at a loss as to how to fix it. It should be easy considering the amount of functionality that any app would have that needs to update a display like this.
Ok, some more reading and a refactor switched to using an observable view model like this:
class ContentViewModel: ObservableObject {
var message: String? = nil {
didSet {
displayMessage = message != nil
}
}
#Published var displayMessage: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel: ContentViewModel
var errorAlert: Alert {
Alert(title: Text("Error!"), message: Text(viewModel.message ?? "oops!"), dismissButton: .default(Text("Ok")))
}
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Button(action: {
self.viewModel.displayMessage.toggle()
}) {
Text("Do it!")
}
}
.alert(isPresented: $viewModel.displayMessage) { errorAlert }
}
}
Which is now working as expected. So the takeaway from this is that using observable view models is more useful even in simpler code like this.

SwiftUI View - viewDidLoad()?

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.

Resources