I am working on in-app purchases with subscriptions.
In swift, you can get price and price locale from the SKProduct like so:
weeklyProduct.price.doubleValue
weeklyProduct.priceLocale.currencySymbol
where weeklyProduct is a SKProduct.
Is it possible to get the free trial length? For example, I specified a two week free trial for the product. can I get this from the SKProduct?
I've solved it using DateComponentsFormatter, that saves you a lot of time localizing in different languages and handling plurals and whatnot.
This might seem like a lot of code, but I hope it will save me time in the future.
import Foundation
class PeriodFormatter {
static var componentFormatter: DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.maximumUnitCount = 1
formatter.unitsStyle = .full
formatter.zeroFormattingBehavior = .dropAll
return formatter
}
static func format(unit: NSCalendar.Unit, numberOfUnits: Int) -> String? {
var dateComponents = DateComponents()
dateComponents.calendar = Calendar.current
componentFormatter.allowedUnits = [unit]
switch unit {
case .day:
dateComponents.setValue(numberOfUnits, for: .day)
case .weekOfMonth:
dateComponents.setValue(numberOfUnits, for: .weekOfMonth)
case .month:
dateComponents.setValue(numberOfUnits, for: .month)
case .year:
dateComponents.setValue(numberOfUnits, for: .year)
default:
return nil
}
return componentFormatter.string(from: dateComponents)
}
}
It requires to convert the SKProduct period unit into a NSCalendarUnit
import StoreKit
#available(iOS 11.2, *)
extension SKProduct.PeriodUnit {
func toCalendarUnit() -> NSCalendar.Unit {
switch self {
case .day:
return .day
case .month:
return .month
case .week:
return .weekOfMonth
case .year:
return .year
#unknown default:
debugPrint("Unknown period unit")
}
return .day
}
}
And you can call it from a SubscriptionPeriod like this:
import StoreKit
#available(iOS 11.2, *)
extension SKProductSubscriptionPeriod {
func localizedPeriod() -> String? {
return PeriodFormatter.format(unit: unit.toCalendarUnit(), numberOfUnits: numberOfUnits)
}
}
Which you can in turn call from a SKProductDiscount like so. Please note I didn't implement the other PaymentModes for now.
import StoreKit
#available(iOS 11.2, *)
extension SKProductDiscount {
func localizedDiscount() -> String? {
switch paymentMode {
case PaymentMode.freeTrial:
return "Free trial for \(subscriptionPeriod.localizedPeriod() ?? "a period")"
default:
return nil
}
}
}
You can get it, but as mentioned above it works only starting from iOS 11.2, for other versions you'll have to get it from your server via API.
Here is an example code that I've used:
if #available(iOS 11.2, *) {
if let period = prod.introductoryPrice?.subscriptionPeriod {
print("Start your \(period.numberOfUnits) \(unitName(unitRawValue: period.unit.rawValue)) free trial")
}
} else {
// Fallback on earlier versions
// Get it from your server via API
}
func unitName(unitRawValue:UInt) -> String {
switch unitRawValue {
case 0: return "days"
case 1: return "weeks"
case 2: return "months"
case 3: return "years"
default: return ""
}
}
Using Eslam's answer as inspiration I created an extension to SKProduct.PeriodUnit
extension SKProduct.PeriodUnit {
func description(capitalizeFirstLetter: Bool = false, numberOfUnits: Int? = nil) -> String {
let period:String = {
switch self {
case .day: return "day"
case .week: return "week"
case .month: return "month"
case .year: return "year"
}
}()
var numUnits = ""
var plural = ""
if let numberOfUnits = numberOfUnits {
numUnits = "\(numberOfUnits) " // Add space for formatting
plural = numberOfUnits > 1 ? "s" : ""
}
return "\(numUnits)\(capitalizeFirstLetter ? period.capitalized : period)\(plural)"
}
}
To use:
if #available(iOS 11.2, *),
let period = prod?.introductoryPrice?.subscriptionPeriod
{
let desc = period.unit.description(capitalizeFirstLetter: true, numberOfUnits: period.numberOfUnits)
} else {
// Fallback
}
This will create a nicely formatted string (e.g. 1 day, 1 Week, 2 months, 2 Years)
Nice one #scott Wood. I would make it a property of SKProduct.PeriodUnit instead of a function. That would keep the behaviour more consistent with enums:
#available(iOS 11.2, *)
extension SKProduct.PeriodUnit {
var description: String {
switch self {
case .day: return "day"
case .week: return "week"
case .month: return "month"
case .year: return "year"
// support for future values
default:
return "N/A"
}
}
func pluralisedDescription(length: Int) -> String {
let lengthAndDescription = length.description + " " + self.description
let plural = length > 1 ? lengthAndDescription + "s" : lengthAndDescription
return plural
}
}
And then a function to return the plural, based on the description property.
And yes, as someone else pointed out, you should localise the plurals if your app is available in other languages.
Trial length is not included in the SKProduct information and will have to be hardcoded into the app or stored on your server. The only available option for deriving this type of information (currently) is from the receipt itself.
Starting from iOS 11.2 you can get info about trials using introductoryPrice property of SKProduct.
It contains instance of SKProductDiscount class, which describes all discount periods including free trials.
Swift 5
Using Eslam's and Scott's answers as inspiration:
import StoreKit
extension SKProduct {
func priceString() -> String {
let period:String = {
switch self.subscriptionPeriod?.unit {
case .day: return "day"
case .week: return "week"
case .month: return "month"
case .year: return "year"
case .none: return ""
case .some(_): return ""
}
}()
let price = self.localizedPrice!
let numUnits = self.subscriptionPeriod?.numberOfUnits ?? 0
let plural = numUnits > 1 ? "s" : ""
return String(format: "%# for %d %#%#", arguments: [price, numUnits, period, plural])
}
}
To use:
let price = product.priceString()
print(price)
Result:
THB 89.00 for 7 days
THB 149.00 for 1 month
+ (NSString*)localizedTitleForSKPeriod:(SKProductSubscriptionPeriod*)period{
NSDateComponents *comps = [NSDateComponents new];
NSDateComponentsFormatter *fmt = [NSDateComponentsFormatter new];
switch (period.unit) {
case SKProductPeriodUnitDay:{
fmt.allowedUnits = NSCalendarUnitDay;
comps.day = period.numberOfUnits;
}break;
case SKProductPeriodUnitWeek:{
fmt.allowedUnits = NSCalendarUnitWeekOfMonth;
comps.weekOfMonth = period.numberOfUnits;
}break;
case SKProductPeriodUnitMonth:{
fmt.allowedUnits = NSCalendarUnitMonth;
comps.month = period.numberOfUnits;
}break;
case SKProductPeriodUnitYear: {
fmt.allowedUnits = NSCalendarUnitYear;
comps.year = period.numberOfUnits;
}break;
}
// 1 Day, 1 Week, 2 Weeks, 1 Month, 2 Months, 3 Months, 6 Months, 1 Year
fmt.unitsStyle = NSDateComponentsFormatterUnitsStyleFull;
// One Day, One Week, Two Weeks, etc
//fmt.unitsStyle = NSDateComponentsFormatterUnitsStyleSpellOut;
NSString *s = [[fmt stringFromDateComponents:comps] capitalizedString];
return s;
}
OBJECTIVE C
#import "SKProduct+SKProduct.h"
-(NSString*_Nullable)localizedTrialDuraion{
if (#available(iOS 11.2, *)) {
NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init];
[formatter setUnitsStyle:NSDateComponentsFormatterUnitsStyleFull]; //e.g 1 month
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropAll;
NSDateComponents * dateComponents = [[NSDateComponents alloc]init];
[dateComponents setCalendar:[NSCalendar currentCalendar]];
switch (self.introductoryPrice.subscriptionPeriod.unit) {
case SKProductPeriodUnitDay:{
formatter.allowedUnits = NSCalendarUnitDay;
[dateComponents setDay:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
break;
}
case SKProductPeriodUnitWeek:{
formatter.allowedUnits = NSCalendarUnitWeekOfMonth;
[dateComponents setWeekOfMonth:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
break;
}
case SKProductPeriodUnitMonth:{
formatter.allowedUnits = NSCalendarUnitMonth;
[dateComponents setMonth:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
break;
}
case SKProductPeriodUnitYear:{
formatter.allowedUnits = NSCalendarUnitYear;
[dateComponents setYear:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
break;
}
default:{
return nil;
break;
}
break;
}
[dateComponents setValue:self.introductoryPrice.subscriptionPeriod.numberOfUnits forComponent:formatter.allowedUnits];
return [formatter stringFromDateComponents:dateComponents];
} else {
// Fallback on earlier versions
}
return nil;
}
Here is more compact and short in use version for swift 5, extending SKProductSubscriptionPeriod
Usage:
print("\(period.localizedDescription) free trial")
//Printed example "1 week free trial"
Implementation:
extension SKProductSubscriptionPeriod {
public var localizedDescription: String {
let period:String = {
switch self.unit {
case .day: return "day"
case .week: return "week"
case .month: return "month"
case .year: return "year"
#unknown default:
return "unknown period"
}
}()
let plural = numberOfUnits > 1 ? "s" : ""
return "\(numberOfUnits) \(period)\(plural)"
}
}
If you use SwiftyStoreKit, localizedSubscriptionPeriod is the easiest way
import SwiftyStoreKit
product.introductoryPrice?.localizedSubscriptionPeriod // "1 week"
This is the implementation:
https://github.com/bizz84/SwiftyStoreKit/blob/master/Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift
I have already authorized HealthKit, and I am getting BiologicalSex from HealthKitStore like this:
let healthKitStore:HKHealthStore = HKHealthStore()
var biologicalSexObject: HKBiologicalSexObject?
var biologicalSex: HKBiologicalSex?
do {
biologicalSexObject = try healthKitStore.biologicalSex()
biologicalSex = biologicalSexObject!.biologicalSex
} catch _ as NSError {
biologicalSex = nil
print("error reading biological sex")
}
However, when I try to print biologicalSex it returns HKBiologicalSex instead of .Male or .Female.
I have seen more or less this exact code in several tutorials, so I'm wondering if there have been any syntax changes I should be aware of in Swift 2. (The error handling has changed, so I'm curious if anything else of note has.)
The rawValue of biologicalSex = biologicalSexObject!.biologicalSex is required to do this. The enum for BiologicalSex looks like this:
typedef enum : NSInteger {
HKBiologicalSexNotSet = 0,
HKBiologicalSexFemale,
HKBiologicalSexMale,
HKBiologicalSexOther,
} HKBiologicalSex;
Using this information it is easy to design a switch statement to cover all of the possible values:
switch biologicalSex.rawValue{
case 0:
biologicalSex = nil
case 1:
biologicalSex = "Female"
case 2:
biologicalSex = "Male"
case 3:
biologicalSex = "Other"
default:
biologicalSex = nil
}
So I have this,
func doStuff() {
//blahblahblah
}
var randomNumber = arc4random() % 4
randomNumber += 1
switch(randomNumber) {
case 1:
doStuff()
break
case 2:
doStuff()
break
case 3:
doStuff()
break
case 4:
doStuff()
break
}
Now I need it to do something like this
func doStuff() {
//blahblahblah
}
var alreadyUsed = [""]
var randomNumber = arc4random() % 4
randomNumber += 1
if randomNumber is not inside alreadyUsed {
switch(randomNumber) {
case 1:
doStuff()
alreadyUsed.append("1")
break
case 2:
doStuff()
alreadyUsed.append("2")
break
case 3:
doStuff()
alreadyUsed.append("3")
break
case 4:
doStuff()
alreadyUsed.append("4")
break
}
}
Essentially, I am trying to have something that will randomly select a case, then won't select the same case again the next time the function is run. I am using that first chunk of code as a function. After a case has been used in that function, I do not want the function to use it again. I hope this makes sense.
Thanks guys!
EDIT: Though I'd find it more useful, It doesn't even have to be an array, as long as it gets the job done.
var someArray = [1,2,3,4]
let index = Int(arc4random_uniform(UInt32(someArray.count)))
let randomNumber = someArray[index]
someArray.removeAtIndex(index)
This should do what you want.
First you create an array with the available numbers
Let it randomly pick a number for the index
Remove the number in the array
I've changed the conversion of UInt32 to Int because according to this post:
Int(arc4random()) will crash 50% of the time it's executed on a 32-bit platform because a UInt32 won't fit in an Int32
I am very new to Swift (got started this week) and I'm migrating my app from Objective-C. I have basically the following code in Objective-C that works fine:
typedef enum : int {
MyTimeFilter1Hour = 1,
MyTimeFilter1Day = 2,
MyTimeFilter7Day = 3,
MyTimeFilter1Month = 4,
} MyTimeFilter;
...
- (void)selectFilter:(id)sender
{
self.timeFilterSelected = (MyTimeFilter)((UIButton *)sender).tag;
[self closeAnimated:YES];
}
When translating it to Swift, I did the following:
enum MyTimeFilter : Int {
case OneHour = 1
case OneDay = 2
case SevenDays = 3
case OneMonth = 4
}
...
#IBAction func selectFilter(sender: AnyObject) {
self.timeFilterSelected = (sender as UIButton).tag as MyTimeFilter
self.close(true)
}
By doing that, I get the error :
'Int' is not convertible to 'MyTimeFilter'
I don't know if my approach (using the tag property) is the best, but anyway I need to do this kind of casting in different places in my app. Does anyone have an idea of how to get rid of this error?
Thanks!
Use the rawValue initializer: it's an initializer automatically generated for enums.
self.timeFilterSelected = MyTimeFilter(rawValue: (sender as UIButton).tag)!
see: The Swift Programming Language ยง Enumerations
NOTE: This answer has changed. Earlier version of Swift use the class method fromRaw() to convert raw values to enumerated values.
Swift 5
#IBAction func selectFilter(sender: AnyObject) {
timeFilterSelected = MyTimeFilter(rawValue: sender.tag)
}
Also for those wondering how you can convert an enum back to an Integer, you can do the following:
enum MyTimeFilter : Int {
case OneHour = 1
case OneDay = 2
case SevenDays = 3
case OneMonth = 4
}
let currentValue = .One
let intValue = currentValue.rawValue
elaborating on Jeffery Thomas's answer.
to be safe place a guard statement unwrap the cast before using it,
this will avoid crashes
#IBAction func selectFilter(sender: AnyObject) {
guard let filter = MyTimeFilter(rawValue: (sender as UIButton).tag) else {
return
}
timeFilterSelected = filter
}
I know, enumeration constant should be like this in swift
enum CompassPoint {
case North
case South
case East
case West
}
But how can I assign value to first element, like Objective-C code as below
enum ShareButtonID : NSInteger
{
ShareButtonIDFB = 100,
ShareButtonIDTwitter,
ShareButtonIDGoogleplus
}ShareButtonID;
You need to give the enum a type and then set values, in the example below North is set as 100, the rest will be 101, 102 etc, just like in C and Objective-C.
enum CompassPoint: Int {
case North = 100, South, East, West
}
let rawNorth = CompassPoint.North.rawValue // => 100
let rawSouth = CompassPoint.South.rawValue // => 101
// etc.
Update: Replace toRaw() with rawValue.
struct AppConstant {
//Usage: AppConstant.IntValues.fifteen.rawValue
enum IntValues: Int {
case zero = 0
case one = 1
case two = 2
case three = 3
case four = 4
case five = 5
case six = 6
case seven = 7
case eight = 8
case nine = 9
case ten = 10
case eleven = 11
case twelve = 12
case thirteen = 13
case fourteen = 14
case fifteen = 15
func asCGFloat() -> CGFloat {
return CGFloat(self.rawValue)
}
func asFloat() -> Float {
return Float(self.rawValue)
}
func asDouble() -> Double {
return Double(self.rawValue)
}
func asString() -> String {
return "\(self.rawValue)"
}
}
}