Swift IAP localized price - swift

I am trying to add localised prices via labels to my shop section of my game. I am struggling to get it to work.
This is 1 part of the code for the purchase bit, its pretty standard.
I have an enum for all my product identifiers.
enum ProductID: String {
case product1
case product2
}
and the standard payment code.
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
println("add payment")
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
print("buy, ok unlock iap here")
print(p.productIdentifier)
let productIdentifier = transaction.payment.productIdentifier
switch productIdentifier {
case ProductID.product1.rawValue:
print("Product 1 bought")
//do something
case ProductID.product2.rawValue:
print("Product 2 bought")
//do something
default:
print("IAP not setup")
}
}
I use the following extension for fetching the localized price, again pretty boiler plate
extension SKProduct {
var localizedPrice: String {
let formatter = NSNumberFormatter()
formatter.numberStyle = .CurrencyStyle
formatter.locale = self.priceLocale
return formatter.stringFromNumber(self.price) ?? "\(price)"
}
}
So in my price labels that are underneath product buttons how do i got about adding each products localised price
priceProduct1Label.text = "\(?.localizedPrice)"
priceProduct2Label.text = "\(?.localizedPrice)"
What do I type for the question marks basically?. The videos and tutorial I read either don't tell you or they don't use a switch statement for the products.
Thank you very much in advance for any replies

I offer a slight variation on crashoverride777's answer above for, rather than a label, a button, and to draw not only the localized price from iTunes Connect but also the other content - the localizedTitle - that makes up the button title:
func loadBuy00ButtonTitle() {
for product in productArray {
let prodID = product.productIdentifier
if(prodID == "com.YourCompanyNameGoesHere.YourAppNameGoesHere.buy00Button") {
let prod = product
buy00Button.setTitle("Buy \(prod.localizedTitle): \(prod.localizedPrice())", for: .normal)
}
}
}
I'm in the USA and thus my button title reads something like: "Buy 100 coins: $0.99" - where for that product (purchase) "100 coins" is the localized title and "$0.99" is the localized price.
And again with minor variations, I, too, use the same extension he provides (in his question), which I place in a separate Swift file:
import StoreKit
extension SKProduct {
func localizedPrice() -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = self.priceLocale
return formatter.string(from: self.price)!
}
}
Ben Dodson's SKProduct localized price extension in Swift

I figured it out finally. I basically took the same code that is used to identify a product when one of the buy buttons is pressed. I just replaced the line that loads the buyProduct() function with the label details. Such as this example of one of my price labels.
func loadPriceLabel1() {
for product in products where product.productIdentifier == ProductID.product1.rawValue {
self.titleLabel.text = "\(product.localizedTitle)"
self.priceLabel1.text = "\(product.localizedPrice)"
etc
}
}
I load the label functions after the product request is finished, right after the buy buttons become enabled. Probably a solution slightly on the clunky side but it works great. If anyone has any suggestions for improvements please let me know.

You are looking for an instance of SKProduct, of which you can load using one or more product identifiers through an SKProductsRequest:
import StoreKit
class MyStore: NSObject, SKProductsRequestDelegate {
func loadProductsForProductIdentifiers( productIdentifiers: Set<String ) {
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers )
productRequest.delegate = self ///< Make sure to set a delegate
productRequest.start()
}
// MARK: - SKProductsRequestDelegate
func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
for object in response.products {
if let product = object as? SKProduct {
// Store these products and use their `productIdentifier` property to identify them
}
}
}
}

Related

Coordinating access to NSManagedObjects across multiple background services

My first Swift app is a photo library manager, and after rebuilding its Core Data guts half a dozen times, I am throwing up my hands and asking for help. For each photo, there are a few "layers" of work I need to accomplish before I can display it:
Create an Asset (NSManagedObject subclass) in Core Data for each photo in the library.
Do some work on each instance of Asset.
Use that work to create instances of Scan, another NSManagedObject class. These have to-many relationships to Assets.
Look over the Scans and use them to create AssetGroups (another NSManagedObject) in Core Data. Assets and AssetGroups have many-to-many relationships.
For each photo, each layer must complete before the next one starts. I can do multiple photos in parallel, but I also want to chunk up the work so it loads into the UI coherently.
I'm really having trouble making this work gracefully; I've built and rebuilt it a bunch of different ways. My current approach uses singleton subclasses of this Service, but as soon as I call save() on the first one, the work stops.
Service.swift
class Service: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var name: String
var predicate: NSPredicate
var minStatus: AssetStatus
var maxStatus: AssetStatus
internal let queue: DispatchQueue
internal let mainMOC = PersistenceController.shared.container.viewContext
internal let privateMOC = PersistenceController.shared.container.newBackgroundContext()
internal lazy var frc: NSFetchedResultsController<Asset> = {
let req = Asset.fetchRequest()
req.predicate = self.predicate
req.sortDescriptors = [NSSortDescriptor(key: #keyPath(Asset.creationDate), ascending: false)]
let frcc = NSFetchedResultsController(fetchRequest: req,
managedObjectContext: self.mainMOC,
sectionNameKeyPath: "creationDateKey",
cacheName: nil)
frcc.delegate = self
return frcc
}()
#Published var isUpdating = false
#Published var frcCount = 0
init(name: String, predicate: NSPredicate? = NSPredicate(value: true), minStatus: AssetStatus, maxStatus: AssetStatus) {
self.name = name
self.predicate = predicate!
self.minStatus = minStatus
self.maxStatus = maxStatus
self.queue = DispatchQueue(label: "com.Ladybird.Photos.\(name)", attributes: .concurrent)
super.init()
self.fetch()
self.checkDays()
}
private func fetch() {
do {
try self.frc.performFetch()
print("\(name): FRC fetch count: \(frc.fetchedObjects!.count)")
} catch {
print("\(name): Unable to perform fetch request")
print("\(error), \(error.localizedDescription)")
}
}
func savePrivate() {
self.privateMOC.perform {
do {
try self.privateMOC.save()
}
catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
func save() {
do {
try self.privateMOC.save()
self.mainMOC.performAndWait {
do {
try self.mainMOC.save()
} catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
}
catch {
print("\(self.name) could not synchonize data. \(error), \(error.localizedDescription)")
}
}
func checkDays() {
// Iterate over days in the photo library
if self.isUpdating { return }
self.isUpdating = true
// self.updateCount = self.frcCount
var daysChecked = 0
var day = Date()
while day >= PhotoKitService.shared.oldestPhAssetDate() {
print("\(name) checkDay \(DateFormatters.shared.key.string(from: day))")
checkDay(day)
var dc = DateComponents()
dc.day = -1
daysChecked += 1
day = Calendar.current.date(byAdding: dc, to: day)!
if daysChecked % 100 == 0 {
DispatchQueue.main.async {
self.save()
}
}
}
self.save()
self.isUpdating = false
}
func checkDay(_ date: Date) {
let dateKey = DateFormatters.shared.key.string(from: date)
let req = Asset.fetchRequest()
req.predicate = NSPredicate(format: "creationDateKey == %#", dateKey)
guard let allAssetsForDateKey = try? self.mainMOC.fetch(req) else { return }
if allAssetsForDateKey.count == PhotoKitService.shared.phAssetsCount(dateKey: dateKey) {
if allAssetsForDateKey.allSatisfy({$0.assetStatusValue >= minStatus.rawValue && $0.assetStatusValue <= maxStatus.rawValue}) {
let frcAssetsForDateKey = self.frc.fetchedObjects!.filter({$0.creationDateKey! == dateKey})
if !frcAssetsForDateKey.isEmpty {
print("\(name): Day \(dateKey) ready for proccessing.")
for a in frcAssetsForDateKey {
self.handleAsset(a)
}
}
}
}
self.save()
}
// implemented by subclasses
func handleAsset(_ asset: Asset) -> Void { }
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.frcCount = self.frc.fetchedObjects?.count ?? 0
self.checkDays()
}
}
I have a subclass of this for each of the four steps above. I want the data to flow between them nicely, and in previous implementations it did, but I couldn't chunk it the way I wanted, and it crashed randomly. This feels more controllable, but it doesn't work: calling save() stops the iteration happening in checkDays(). I can solve that by wrapping save() in an async call like DispatchQueue.main.async(), but it has bad side effects — checkDays() getting called while it's already executing. I've also tried calling save() after each Asset is finished, which makes the data move between layers nicely, but is slow as hell.
So rather than stabbing in the dark, I thought I'd ask whether my strategy of "service layers" feels sensible to others who others who have dealt with this kind of problem. It'd also be helpful to hear if my implementation via this Service superclass makes sense.
What would be most helpful is to hear from those with experience how they would approach implementing a solution to this problem: consecutive steps, applied concurrently to multiple Core Data entities, all in the background. There are so many ways to solve pieces of this in Swift — async/await, Tasks & Actors, DispatchQueues, ManagedObjectContext.perform(), container.performBackgroundTask(), Operation… I've tried each of them to mixed success, and what I feel like I need here is a trail map to get out of the forest.
Thanks y'all

Swift - Building instagram-like user certification tableview for comments

I'm building an Instagram-like certified-user feature on comments page.
For each comment, I need to check if the comment uploader is a certified user, and add user's certification icon right beside user's name.
My logic is following.
1 - Fetch all comments
2 - For each comment, check if commented user is certified user
3 - If comment uploader is certified, then mark commented-user as certified user
4 - Every time comments' data changes, reload tableView
My question is, would "reloading tableview" for every time comment changes cause huge performance issue?
Let's say there are 100 comments. Then, every time comment changes, tableView would reload data. So it'd reload 100 times with this logic.
If it is bad practice, how can I enhance my codes?
Following are my codes.
private var comments = [Comment]() {
didSet { tableView.reloadData() }
}
private func fetchComments() {
guard let questionId = question?.id else { return }
API.shared.fetchComments(questionOrCommentId: questionId) { [weak self] comments in
guard let self = self else { return }
self.comments = comments
DispatchQueue.main.async {
self.checkIfCommentsWritersAreVerified()
}
}
}
private func checkIfCommentsWritersAreVerified() {
self.comments.forEach { comment in
checkIfUserIsVerified(forUid: comment.uid, forCommentId: comment.commentId)
}
}
private func checkIfUserIsVerified(forUid uid: String, forCommentId commentId: String = "") {
API.shared.checkIfUserIsVerified(forUserId: uid) { [weak self] userIsVerified in
guard let self = self else { return }
if let index = self.comments.firstIndex(where: { $0.commentId == commentId }) {
self.comments[index].userIsVerifiedTipper = userIsVerified
}
}
}

How to display local SKProduct price in UILabel?

I want to get localised in app purchase price with currency symbol and display it on UILabel. But I don't know how to do that since I don't know how to get SKProduct object. Maybe it's possible to get it using apple product id?
extension SKProduct {
/// - returns: The cost of the product formatted in the local currency.
var regularPrice: String? {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = self.priceLocale
return formatter.string(from: self.price)
}}
EDIT:
let productIds: [String] = ["id1", "id2"]
getProducts(productIdentifiers: productIds)
SKProduct objects are returned as part of an SKProductsResponse
object. (c) Apple documentation
How to fetching product information: Fetching Product Information from the App Store
Also take a look to useful video from WWDC: Using Store Kit for In-App Purchases with Swift 3
Summarizing above documentation:
class ProductResolver: NSObject, SKProductsRequestDelegate {
// Keep a strong reference to the product request.
var request: SKProductsRequest!
func getProducts(productIdentifiers: [String]) {
let productIdentifiers = Set(productIdentifiers)
request = SKProductsRequest(productIdentifiers: productIdentifiers)
request.delegate = self
request.start()
}
// SKProductsRequestDelegate protocol method.
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
for product in response.products {
print("Local currency \(product.regularPrice)")
}
}
}

How Save UILocalNotifications in CoreData

Answer is below, image is here:
I was searching how to do this for a couple of days and was only able to find people who stored UILocalNotificaations in NSUserDefaults. Saving these in NSUserDefaults seemed wrong to me because it is supposed to be used for small flags. I just now finally figured out how to store notifications in CoreData. This is Using Xcode 7.3.1 and Swift 2.2
First off you need to create a new entity in your CoreDataModel
and then add a single attribute to it. the attribute should be of type Binary Data I named my table/entity "ManagedFiredNotifications" and my attribute "notification". it should look like this:
Image linked in Question above.
Next you need to add an extension to UILocalNotification it should go like this:
extension UILocalNotification {
func save() -> Bool {
let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
let firedNotificationEntity = NSEntityDescription.insertNewObjectForEntityForName("ManagedFiredNotifications", inManagedObjectContext: appDelegate!.managedObjectContext)
guard appDelegate != nil else {
return false
}
let data = NSKeyedArchiver.archivedDataWithRootObject(self)
firedNotificationEntity.setValue(data, forKey: "notification")
do {
try appDelegate!.managedObjectContext.save()
return true
} catch {
return false
}
}
}
Now for saving a notification all you need to do is call
UILocalNotification.save()
On the notification you would like to save. my notifications were named 'notification' so I would call notification.save()
To retrieve a notification you need a method like this
func getLocalFiredNotifications() -> [UILocalNotification]? {
let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)!.managedObjectContext
let firedNotificationFetchRequest = NSFetchRequest(entityName: "ManagedFiredNotifications")
firedNotificationFetchRequest.includesPendingChanges = false
do {
let fetchedFiredNotifications = try managedObjectContext.executeFetchRequest(firedNotificationFetchRequest)
guard fetchedFiredNotifications.count > 0 else {
return nil
}
var firedNotificationsToReturn = [UILocalNotification]()
for managedFiredNotification in fetchedFiredNotifications {
let notificationData = managedFiredNotification.valueForKey("notification") as! NSData
let notificationToAdd = NSKeyedUnarchiver.unarchiveObjectWithData(notificationData) as! UILocalNotification
firedNotificationsToReturn.append(notificationToAdd)
}
return firedNotificationsToReturn
} catch {
return nil
}
}
Note that this returns an array of UILocalNotifications.
When retrieving these if you plan on removing a few of them and then storing the list again you should remove them when you get them something like this works:
func loadFiredNotifications() {
let notifications = StudyHelper().getLocalFiredNotifications()
if notifications != nil {
firedNotifications = notifications!
} else {
// throw an error or log it
}
classThatRemoveMethodIsIn().removeFiredLocalNotifications()
}
I hope this helps someone who had the same problems that I did trying to implement this.

swift - Populate STPPaymentCardTextField Programmatically

I'm developing an app and using Stripe SDK and Card.io SDK. What i want to happen is populate the STPPaymentCardTextField Card Number,Expiry Month and Year with Card.io scanned credit card value. I Tried:
var paymentField = STPPaymentCardTextField()
func userDidProvideCreditCardInfo(cardInfo: CardIOCreditCardInfo!, inPaymentViewController paymentViewController: CardIOPaymentViewController!) {
var scanViewController: CardIOPaymentViewController = CardIOPaymentViewController(paymentDelegate: self)
paymentField.cardNumber = cardInfo.cardNumber
paymentField.expirationMonth = cardInfo.expiryMonth
paymentField.expirationYear = cardInfo.expiryYear
paymentViewController.dismissViewControllerAnimated(true, completion: nil)
}
I'm having an error Cannot assign to the result of this expression for each paymentField append.
What do you think i can do with this? Thanks!
STPPaymentCardTextField fields are read-only and you can only "get" from those properties.
STPPaymentCardTextField is used to collect credit card details. In your case you are already doing that using CardIOCreditCardInfo. Once you have the credit card details you can assemble the data into an STPCardParams object.
Once you've collected the card number, expiration, and CVC, package them up in an STPCardParams object and invoke the createTokenWithCard: method on the STPAPIClient class.
Now your method may look like this...
func userDidProvideCreditCardInfo(cardInfo: CardIOCreditCardInfo!, inPaymentViewController paymentViewController: CardIOPaymentViewController!) {
let card: STPCardParams = STPCardParams()
card.number = info.cardNumber
card.expMonth = info.expiryMonth
card.expYear = info.expiryYear
card.cvc = info.cvv
STPAPIClient.sharedClient().createTokenWithCard(card, completion: {(result, error) -> Void in
if error == nil {
// Either save the card token with your customer data or charge them right away
//createBackendChargeWithToken(token)
}
else {
//handleError(error)
}
})
}
paymentViewController?.dismissViewControllerAnimated(true, completion: nil)
}
Update May be the right answer to your question.
import Stripe
class PaymentCardEditorField: STPPaymentCardTextField {
func setExistingCard(card: STPCardParams) {
replaceField("numberField", withValue: card.number!)
replaceField("expirationField", withValue: String(format: "%02d/%02d", card.expMonth, (card.expYear % 100)))
replaceField("cvcField", withValue: card.cvc!)
}
func replaceField(memberName: String, withValue value: String) {
let field = self.valueForKey(memberName) as! UITextField
let delegate = self as! UITextFieldDelegate
let len = field.text?.characters.count
if delegate.textField?(field, shouldChangeCharactersInRange: NSMakeRange(0, len!), replacementString: value) ?? false {
field.text = value
}
}
}
And then call the setExistingCard
func userDidProvideCreditCardInfo(cardInfo: CardIOCreditCardInfo!, inPaymentViewController paymentViewController: CardIOPaymentViewController!) {
let card: STPCardParams = STPCardParams()
card.number = info.cardNumber
card.expMonth = info.expiryMonth
card.expYear = info.expiryYear
card.cvc = info.cvv
paymentTextField.setExistingCard(card)
}
Works like a charm.
Follow this thread for potential update to Stripe SDK for in built support in the future.
https://github.com/stripe/stripe-ios/issues/127
I got a much cleaner answer from here
let cardParams = STPCardParams()
cardParams.number = "4242424242424242"
cardParams.expMonth = 07 // this data type is UInt and *not* Int
cardParams.expYear = 19 // this data type is UInt and *not* Int
cardParams.cvc = "123"
let paymentField = STPPaymentCardTextField()
paymentField.cardParams = cardParams //the paymentTextField will now show the cc #, exp, and cvc from above
You can get other test cards numbers like Mastercard, Amex, and Discover from here from Stripe