Updating a UILabel from an escaping closure doesn't take effect immedialtely - ios

Basically:
In viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myFunction(closure)
}
closureis of type #escaping (_ success: Bool) -> Void
In the closure code:
print("Change myLabel.text")
self?.myLabel.text = "New Title"
print("myLabel.text changed")
"Show myLabel.text" and "myLabel.text changed" are printed as soon as the VC appears, but the text in myLabel changes after several seconds (around 10 seconds).
myLabel is created programmatically as seen below:
class MyClass : UIViewController {
...
var myLabel: UILabel!
var contacts = [ContactEntry]()
...
override func viewWillLayoutSubviews() {
myLabel = UILabel()
myLabel.text = "Original title"
myLabel.frame = CGRect(x: 10, y: 10, width: 100, height: 400)
self.view.addSubview(myLabel)
}
}
The actual code is inspired from here:
viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
requestAccessToContacts { [weak self] (success) in
if success {
self?.retrieveContacts(completion: { (success, contacts) in
self?.tableView.isHidden = false
self?.myLabel.isHidden = true
if success && (contacts?.count)! > 0 {
self?.contacts = contacts!
self?.myLabel.text = ""
self?.myLabel.isHidden = true
self?.tableView.reloadData()
} else if (contacts?.count)! == 0 {
self?.myLabel.isHidden = false
self?.myLabel.text = "No contacts found"
} else {{
self?.myLabel.isHidden = false
self?.myLabel.text = "Error loading contacts"
}
})
} else {
print("Change label text")
self?.myLabel.attributedText = "Enable access to contacts by going to\nSettings>Privacy>Contacts>MyApp"
self?.myLabel.isHidden = false
print("Label text changed")
}
}
}
requestAccessToContacts:
func requestAccessToContacts(completion: #escaping (_ success: Bool) -> Void) {
let authorizationStatus = CNContactStore.authorizationStatus(for: CNEntityType.contacts)
switch authorizationStatus {
case .authorized:
// authorized previously
completion(true)
case .denied, .notDetermined:
// needs to ask for authorization
self.contactStore.requestAccess(for: CNEntityType.contacts, completionHandler: { (accessGranted, error) -> Void in
completion(accessGranted)
})
default:
// not authorized.
completion(false)
}
}
retrieveContacts:
func retrieveContacts(completion: (_ success: Bool, _ contacts: [ContactEntry]?) -> Void) {
var contacts = [ContactEntry]()
do {
let contactsFetchRequest = CNContactFetchRequest(keysToFetch: [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactImageDataKey, CNContactImageDataAvailableKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey].map {$0 as CNKeyDescriptor})
try contactStore.enumerateContacts(with: contactsFetchRequest, usingBlock: { (cnContact, error) in
if let contact = ContactEntry(cnContact: cnContact) { contacts.append(contact) }
})
completion(true, contacts)
} catch {
completion(false, nil)
}
}
What am I missing here?

You are saying:
print("Change myLabel.text")
self?.myLabel.text = "New Title"
print("myLabel.text changed")
And you are complaining that the print messages appear in the console but the label doesn't change until much later.
This sort of delay is nearly always caused by a threading issue. You do not show MyFunction and you do not show the entirety of closure, so it's impossible to help you in detail, but the likelihood is that you are messing around with background threads without knowing what you are doing, and that you have accidentally set myLabel.text on a background thread, which is a big no-no. You must step out to the main thread in order to touch the interface in any way:
DispatchQueue.main.async {
print("Change myLabel.text")
self?.myLabel.text = "New Title"
print("myLabel.text changed")
// ... and everything else that touches the interface
}

Related

Stripe iOS didCreatePaymentResult never gets called

The problem seems simple, didCreatePaymentResult never gets called.
BUT, in my old sample project, taken from your iOS example for payment intent, that didCreatePaymentResult gets called every single time I create or select a card, here's the repo of the working project: https://github.com/glennposadas/stripe-example-ios-nodejs
BUT again, my main concern is my current project.
I use v19.2.0 in both of these projects, I even tried the v19.3.0.
I wanted to use Stripe Charge really, but I believe Stripe does not support Apple pay for that. So I have no choice but to use Stripe Payment Intent.
CoreService.swift (conforms to STPCustomerEphemeralKeyProvider)
extension CoreService: STPCustomerEphemeralKeyProvider {
func createCustomerKey(withAPIVersion apiVersion: String, completion: #escaping STPJSONResponseCompletionBlock) {
orderServiceProvider.request(.requestEphemeralKey(stripeAPIVersion: apiVersion)) { (result) in
switch result {
case let .success(response):
guard let json = ((try? JSONSerialization.jsonObject(with: response.data, options: []) as? [String : Any]) as [String : Any]??) else {
completion(nil, NSError(domain: "Error parsing stripe data", code: 300, userInfo: nil))
return
}
completion(json, nil)
default:
UIViewController.current()?.alert(title: "Error stripe", okayButtonTitle: "OK", withBlock: nil)
}
}
}
}
PaymentController.swift
class PaymentViewController: BaseViewController {
// MARK: - Properties
private var paymentContext: STPPaymentContext!
private let paymentConstantValue: Int = 3000
// MARK: - Functions
// MARK: Overrides
override func viewDidLoad() {
super.viewDidLoad()
self.setupStripe()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.hideNavBar(animated: true)
}
#IBAction func creditCardButtonTapped(_ sender: Any) {
self.paymentContext.presentPaymentOptionsViewController()
}
private func setupStripe() {
let config = STPPaymentConfiguration.shared()
config.appleMerchantIdentifier = "merchant.com.gsample.app"
config.companyName = "Scoutd LLC"
config.requiredBillingAddressFields = .none
config.requiredShippingAddressFields = .none
config.additionalPaymentOptions = .applePay
let customerContext = STPCustomerContext(keyProvider: CoreService())
let paymentContext = STPPaymentContext(
customerContext: customerContext,
configuration: config,
theme: STPTheme.default()
)
let userInformation = STPUserInformation()
paymentContext.prefilledInformation = userInformation
paymentContext.paymentAmount = self.paymentConstantValue
paymentContext.paymentCurrency = "usd"
self.paymentContext = paymentContext
self.paymentContext.delegate = self
self.paymentContext.hostViewController = self
}
}
// MARK: - STPPaymentContextDelegate
extension PaymentViewController: STPPaymentContextDelegate {
func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
print("paymentContextDidChange")
}
func paymentContext(_ paymentContext: STPPaymentContext, didFailToLoadWithError error: Error) {
// error alert....
}
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: #escaping STPPaymentStatusBlock) {
print("didCreatePaymentResult ✅")
}
func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) {
switch status {
case .success:
// success
case .error:
// error alert....
default:
break
}
}
}
SOLVED! This should help engineers struggling with Stripe implementation in the future.
So in my case, I have two buttons:
Apple Pay
Credit card.
The absolute solution for me is handle the selectedPaymentOption of the paymentContext.
Scenarios:
If the apple pay button is tapped, present apple pay sheet and don't present add/select card UI of Stripe.
If the credit card button is tapped, don't present apple pay sheet and instead present select card.
Related to #2, call requestPayment() if there's a selected option.
Voila! The didCreatePaymentResult now gets invoked!
// MARK: IBActions
#IBAction func applePayButtonTapped(_ sender: Any) {
if self.paymentContext.selectedPaymentOption is STPApplePayPaymentOption {
self.paymentContext.requestPayment()
}
}
#IBAction func creditCardButtonTapped(_ sender: Any) {
if let selectedPaymentOption = self.paymentContext.selectedPaymentOption,
!(selectedPaymentOption is STPApplePayPaymentOption) {
self.paymentContext.requestPayment()
return
}
self.paymentContext.presentPaymentOptionsViewController()
}

iOS Local Biometric authentication dialogue called again and again

Below is my code. When ever I run this code and validate the app using TouchID, TouchID Authentication dialog is dismissed and viewDidLoad() is called again which in turn shows the TouchID alert again. So I am not able to leave this page and stuck in a loop. Any help would be appreciated.
Note: Same code was working fine 2 days ago.
override func viewDidLoad() {
super.viewDidLoad()
initialSetup()
checkAuthenticationMethod()
}
private func checkAuthenticationMethod() {
let biometricsEnabled = UserDefaults.standard.bool(forKey: LocalDefaults.biometricsEnabled.rawValue)
if biometricsEnabled {
OperationQueue.main.addOperation {
self.setupLocalAuthentication()
}
}
}
private func setupLocalAuthentication() {
var error: NSError?
context.localizedCancelTitle = "Login using your PIN"
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
if let err = error {
self.showAlert(message: err.localizedDescription, withTitle: "Error", willViewPop: false)
}else {
self.localAuthenticationMessage.isHidden = false
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Unlock App") { (success, error) in
DispatchQueue.main.async { [weak self] in
self?.view.isUserInteractionEnabled = !success
if success {
self?.showHUDLoader(containerView: nil,message: "Logging in", enableInteraction: false)
self?.pinImage.image = UIImage(named: "PIN_4")
self?.updateUserLoginToken()
}else if let err = error {
self?.pinImage.image = UIImage(named: "PIN_0")
let errorCode = (err as NSError).code
if errorCode != -4 {
self?.localAuthenticationMessage.isHidden = true
}
}
}
}
}
}
}

UILabel reverts to default text on iPhone rotation

This is a slightly odd one which I'm not sure where to start debugging. I have a UILabel on a standard view which I update the text based on certain conditions. From the IB I have set default text that reads 'Loading...' and then the viewDidAppear method updates the text based on the conditions. This works fine, however, if I then rotate my iPhone (or simulator) it reverts the UILabel back to the standard text of 'Loading...'.
What's interesting is that when I view it on an iPad, both simulator and actual device it doesn't change the text back to the default and acts as I would expect.
I have tried detecting an orientation change and resetting the text but that has no effect, it's a bit like the label has become locked to default state.
Happy to provide code if necessary but I'm really not sure what code is relevant as it's a straight forward label and updating it's text.
Thanks
import UIKit
class PredictionViewController: UIViewController {
var predictionData: Predictions!
var embeddedVC: PredictionsTableViewController?
#IBOutlet weak var messageTextBox: UILabel!
#IBOutlet weak var predictionSubmitButton: UIButton!
#IBOutlet weak var predictionSubmitButtonHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//self.messageTextBox.isEditable = false
NotificationCenter.default.addObserver(self, selector: #selector(settingChanged(notification:)), name: UserDefaults.didChangeNotification, object: nil)
}
override func viewDidAppear(_ animated: Bool) {
let preferences = UserDefaults.standard
if (preferences.object(forKey: "regID") == nil)
{
loadLoginScreen()
}
else {
let sv = UIViewController.displaySpinner(onView: self.view)
let predictionStatus = preferences.object(forKey: "predictionStatus") as! String
switch (predictionStatus) {
case "inplay":
setInplay(view: self)
case "finished":
setFinished(view: self)
case "predict":
setPredict(view: self)
default:
self.messageTextBox.text = "Error!"
}
if (self.messageTextBox.isHidden) {
self.messageTextBox.isHidden = false
}
UIViewController.removeSpinner(spinner: sv)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "predictionSegue") {
if let vc = segue.destination as? PredictionsTableViewController {
// get a reference to the embedded VC
self.embeddedVC = vc
}
}
}
#objc func settingChanged(notification: NSNotification) {
let preferences = UserDefaults.standard
let predictionStatus = preferences.object(forKey: "predictionStatus") as! String
switch (predictionStatus) {
case "inplay":
setInplay(view: self)
case "finished":
setFinished(view: self)
case "predict":
setPredict(view: self)
default:
messageTextBox.text = "Error!"
}
}
func setInplay(view: PredictionViewController) {
view.messageTextBox.text = "In Play!"
view.predictionSubmitButtonHeight.constant = 0
}
func setFinished(view: PredictionViewController) {
view.messageTextBox.text = "Finished!"
view.predictionSubmitButtonHeight.constant = 0
}
func setPredict(view: PredictionViewController) {
view.messageTextBox.text = "Predict Now!"
view.predictionSubmitButton.isEnabled = true
view.predictionSubmitButton.setTitle("Submit", for: .normal)
view.predictionSubmitButtonHeight.constant = 58
}
#IBAction func predictionSubmitButtonAction(_ sender: UIButton) {
let preferences = UserDefaults.standard
let sv = UIViewController.displaySpinner(onView: self.view)
CheckTime(finished: { isSuccess in
switch (isSuccess) {
case "inplay":
preferences.set("inplay", forKey: "predictionStatus")
//too late alert
case "finished":
preferences.set("finished", forKey: "predictionStatus")
//too late alert
case "predict":
preferences.set("predict", forKey: "predictionStatus")
if let predictionData = self.embeddedVC?.getPredictionData() {
//send back to website
let regID = preferences.object(forKey: "regID")
let url = URL(string: "[URL]")
let session = URLSession.shared
let request = NSMutableURLRequest(url: url!)
request.httpMethod = "POST"
let bodyData = "{}"
request.httpBody = bodyData.data(using: String.Encoding.utf8);
let task = session.dataTask(with: request as URLRequest, completionHandler: {
(data, response, error) in
guard let data = data, let _ = response, error == nil else
{
DispatchQueue.main.async(
execute: {
UIViewController.removeSpinner(spinner: sv)
self.displayAlertMessage(message: "response error: \(String(describing: error?.localizedDescription))", type: "error")
}
)
return
}
do {
let decoder = JSONDecoder()
let predictionResult = try decoder.decode(ReturnData.self, from: data)
DispatchQueue.main.async(
execute: {
if (predictionResult.success) {
self.displayAlertMessage(message: predictionResult.message, type: "message", title: "Predictions Received")
}
else {
self.displayAlertMessage(message: "response error: \(String(describing: error?.localizedDescription))", type: "error")
}
UIViewController.removeSpinner(spinner: sv)
}
)
} catch {
DispatchQueue.main.async(
execute: {
UIViewController.removeSpinner(spinner: sv)
self.displayAlertMessage(message: "response error: \(error)", type: "error")
}
)
return
}
})
task.resume()
}
default:
UIViewController.removeSpinner(spinner: sv)
self.messageTextBox.text = "Error!"
preferences.set("error", forKey: "predictionStatus")
}
preferences.synchronize()
if (self.messageTextBox.isHidden) {
self.messageTextBox.isHidden = false
}
})
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
print("Landscape")
//imageView.image = UIImage(named: const2)
} else {
print("Portrait")
//imageView.image = UIImage(named: const)
}
self.messageTextBox.text = "Error!"
}
Can You use this Delegate method for screen orientation.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) -> Void in
}, completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in
//refresh view once rotation is completed not in will transition as it returns incorrect frame size.Refresh here
**//---> Set the text for label here.**
})
super.viewWillTransition(to: size, with: coordinator)
}
I believe that you should take your code off viewDidAppear and put inside viewDidLoad.
If you don't want to use the code in other orientation, you should uncheck for all other orientations and only choose the one you want to be implemented, that will fix your problem, however if you want to works in other orientations, try to do what I said and see if it works.
Even none of what I just said works, try to look around your code if you have a condition to changes the text when transition happens.
One more thing, just a tip, avoid putting too much code inside of a simple action, try to refactoring in other Methods and then call it inside your action.

Stripe - retrieveCustomer callback infinite loading

I had implement the Stripe to my project.I'm using an extension of default STPPaymentMethodsViewController like this:
class PaymentMethodVC: STPPaymentMethodsViewController {
convenience init()
{
let theme = STPTheme()
theme.primaryBackgroundColor = UIColor.pintHubDarkBrown
theme.secondaryBackgroundColor = UIColor.pintHubHeaderColor
theme.accentColor = UIColor.white
theme.primaryForegroundColor = UIColor.pintHubOrange
theme.secondaryForegroundColor = UIColor.pintHubOrange
theme.font = UIFont.mainRegular()
let paymentdelegate = PaymentMethodVCDelegate()
let paymentConfig = STPPaymentConfiguration.shared()
paymentConfig.publishableKey = "stripePublickToken"
let apiAdapter = PaymentApiAdapter()
self.init(configuration: paymentConfig, theme: theme, apiAdapter: apiAdapter, delegate: paymentdelegate)
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
}
PaymentMethodVCDelegate is an object that implements STPPaymentMethodsViewControllerDelegate that methods are never called and
PaymentApiAdapter is other object that implements STPBackendAPIAdapter protocol which methods are:
public func retrieveCustomer(_ completion: #escaping Stripe.STPCustomerCompletionBlock)
public func attachSource(toCustomer source: STPSource, completion: #escaping Stripe.STPErrorBlock)
public func selectDefaultCustomerSource(_ source: STPSource, completion: #escaping Stripe.STPErrorBlock)
everything works fine expect when i want to return an error to the callback method func retrieveCustomer(_ completion: #escaping Stripe.STPCustomerCompletionBlock) that is a method of the STPBackendAPIAdapter protocol more details here.
this is my code:
func retrieveCustomer(_ completion: #escaping (STPCustomer?, Error?) -> Swift.Void)
{
stripeEndpoint.getStripeCustomer(for: "myStrypeCustomerId") { (status, JSON) in
if !status.success()
{
let userInfo = [NSLocalizedDescriptionKey:status.error,
NSLocalizedFailureReasonErrorKey: status.code,
NSLocalizedRecoverySuggestionErrorKey: ""
] as [String : Any]
let error = NSError(domain: "MyDomain", code: Int(status.error) ?? 0, userInfo: userInfo)
completion(nil, error)
}
else
{
var customer:STPCustomer? = nil
if let jsonData = JSON
{
let deserializer = STPCustomerDeserializer(jsonResponse: jsonData)
customer = deserializer.customer!
}
completion(customer, nil)
}
}
and when i receive an error the screen displays and infinite loading indicator.
and if i call completion(nil, nil) the loading disappear but i when i press cancel the ViewController don't pop from stack

CXCallObserver is not working properly and App getting crash when running the app more than one (when includes contacts image data)

I am facing two major problem first one is :
1. I am trying to detect incoming call, outgoing call , dialing call for this i am using this code :
import UIKit
import CoreTelephony
import CallKit
class ViewController: UIViewController,CXCallObserverDelegate {
let callObserver = CXCallObserver()
var seconds = 0
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
callObserver.setDelegate(self, queue: nil)
}
override func viewWillAppear(_ animated: Bool) {
print("viewWillAppear \(seconds)")
}
fileprivate func runTimer(){
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateTimer), userInfo: nil, repeats: true)
}
func updateTimer() {
seconds += 1
print("Seconds \(seconds)")
}
#IBAction func callButton(_ sender: UIButton) {
if let url = URL(string: "tel://\(12345879)"){
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) {
if call.hasEnded == true {
print("Disconnected")
seconds = 0
self.timer.invalidate()
}
if call.isOutgoing == true && call.hasConnected == false {
print("Dialing call")
self.runTimer()
}
if call.isOutgoing == false && call.hasConnected == false && call.hasEnded == false {
print("Incoming")
}
if call.hasConnected == true && call.hasEnded == false {
print("Connected")
}
}
}
It working fine when i am dialing a number it shows "Dialling" but when i cut the call then it shows "Disconnected" then again "Dialing" State.
Another problem is when i am fetching all contacts information from the device it works fine when i am not fetching imageData but when i am fetching contacts image it works fine for the very first time . Then if i run it again app become slow . then next it crash shows found nil while unwrapping a value.
i wrote my contact data fetching function in AppDelegate . it is calling when the app start . this is the code :
func fetchContactList(){
let loginInformation = LoginInformation()
var contactModelData: [ContactsModel] = []
var profileImage : UIImage?
let store = CNContactStore()
store.requestAccess(for: .contacts, completionHandler: {
granted, error in
guard granted else {
let alert = UIAlertController(title: "Can't access contact", message: "Please go to Settings -> MyApp to enable contact permission", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
return
}
let keysToFetch = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName),CNContactPhoneNumbersKey, CNContactEmailAddressesKey, CNContactPostalAddressesKey, CNContactImageDataKey, CNContactImageDataAvailableKey,CNContactThumbnailImageDataKey,CNContactThumbnailImageDataKey] as [Any]
let request = CNContactFetchRequest(keysToFetch: keysToFetch as! [CNKeyDescriptor])
var cnContacts = [CNContact]()
do {
try store.enumerateContacts(with: request){
(contact, cursor) -> Void in
cnContacts.append(contact)
}
} catch let error {
NSLog("Fetch contact error: \(error)")
}
for contact in cnContacts {
let fullName = CNContactFormatter.string(from: contact, style: .fullName) ?? "No Name"
var phoneNumberUnclean : String?
var labelofContact : String?
var phoneNumberClean: String?
for phoneNumber in contact.phoneNumbers {
if let number = phoneNumber.value as? CNPhoneNumber,
let label = phoneNumber.label {
let localizedLabel = CNLabeledValue<CNPhoneNumber>.localizedString(forLabel: label)
print("fullname \(fullName), localized \(localizedLabel), number \(number.stringValue)")
phoneNumberUnclean = number.stringValue
labelofContact = localizedLabel
}
}
if let imageData = contact.imageData {
profileImage = UIImage(data: imageData)
print("image \(String(describing: UIImage(data: imageData)))")
} else {
profileImage = UIImage(named: "user")
}
self.contactModelData.append(ContactsModel(contactName: fullName, contactNumber:phoneNumberUnclean!, contactLabel: labelofContact!, contactImage: profileImage!, contactNumberClean: phoneNumberUnclean!))
}
self.loginInformation.saveContactData(allContactData: self.contactModelData)
})
}
I have solved this two problems using this :
for number one when i disconnect a call then if unfortunately it goes to "Dialling" option again i checked the "seconds" variable's value if it greater than 0 in "Dialing" then invalidate the thread.
for number two problem :
I used Dispatch.async.main background thread and take the thumbnail image

Resources