StoreKit problem while restoring In-App Purchase - swiftui

I tried to find a tutorial about IAP, and I found : This one
But when I call function to restore, nothing happens. OnTap button I call : store.restorePurchase()
extension Store {
func product(for identifier: String) -> SKProduct? { ...}
func purchaseProduct (_ product: SKProduct){ ...}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
print("restore")
}
}
restore is print in my console. It is the only thing that is printed in console. But in the code above, you can see .restored:that should print "UserIsPremium"
extension Store : SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions : [SKPaymentTransaction]){
for transaction in transactions {
var shouldFinishTransaction = false
switch transaction.transactionState{
case .purchased, .restored:
completedPurchases.append(transaction.payment.productIdentifier)
print("UserIsPremium")
shouldFinishTransaction = true
case .failed:
print("NotPremium")
shouldFinishTransaction = true
case .deferred, .purchasing:
print("...")
break
#unknown default:
print("unknown")
break
}
if shouldFinishTransaction {
print("shouldfinish")
SKPaymentQueue.default().finishTransaction(transaction)
DispatchQueue.main.async {
self.purchaseCompletionHandler?(transaction)
self.purchaseCompletionHandler = nil
}
}
}
}
}
My class Store :
import StoreKit
typealias FetchCompletionHandler = (([SKProduct]) -> Void)
typealias PurchaseCompletionHandler = ((SKPaymentTransaction?) -> Void)
class Store: NSObject, ObservableObject {
#Published var allFullVersion = [FullVersion]()
private let allProductsIdentifiers = Set ([
"ClemPapplis.OrientationEPS.versionFull"
])
private var completedPurchases = [String]() {
didSet {
DispatchQueue.main.async{ [weak self] in
guard let self = self else { return }
for index in self.allFullVersion.indices {
if self.completedPurchases.contains(self.allFullVersion[index].id){
print("completedPurchases")
UserDefaults.standard.setValue(true, forKey: "Premium")
}
self.allFullVersion[index].isLocked =
!self.completedPurchases.contains(self.allFullVersion[index].id)
}
}
}
}
I use sandbox testers.
If I buy item, no problem, datas are print in console.
Did I forget something ?

It's been a few years since I used StoreKit, but I remember having an issue with restore purchases also (and yes, #ElTomato is correct, use a separate case - I'm surprised your code actually builds). I found that the following worked:
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
delegate.purchaseComplete()
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
default:
break
}}}
}
func restorePurchases() {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
for _ in queue.transactions {
alreadyPurchased = true
delegate.purchasesRestored()
}
showAlert("Purchases have been restored.")
}
Don't worry about showAlert, that's just an extension I have for UIViewController. But what I'm not seeing in your code is paymentQueueRestoreCompletedTransactionsFinished(_ queue:).

I followed the same tutorial i think you did not select the select the none in here
configuration
to restore in your simulator you should select none when you select none it tries to connect apple developers in app purchase configuration. give it a try and let me know.

Related

Return from initializer without initializing all stored properties - Contacts Store

Code from tutorial, want to access contacts from my app, and using this as base
Need to solve this error to move forward and do not know what additional items needs initialization. Any help very appreciated.
import Contacts
final class ContactsViewModel: ObservableObject {
#Published
var contact: [Contact?]
#Published
var contacts: [Contact?] = []
#Published
var permissionsError: PermissionsError? = .none
init( contact: Contact, contacts: [Contact] , permissionsError: PermissionsError) {
permissions()
}. **<-- Error occurs here -**
func openSettings() {
permissionsError = .none
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {return}
if UIApplication.shared.canOpenURL(settingsURL) { UIApplication.shared.open(settingsURL)}
}
func getContacts() {
Contact.fetchAll { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let fetchedContacts):
DispatchQueue.main.async {
self.contacts = fetchedContacts.sorted(by: { $0.lastName < $1.lastName })
}
case .failure(let error):
self.permissionsError = .fetchError(error)
}
}
}
func permissions() {
switch CNContactStore.authorizationStatus(for: .contacts) {
case.authorized:
getContacts()
case .notDetermined, .restricted, .denied :
CNContactStore().requestAccess(for: .contacts) { [weak self] granted, error in
switch granted {
case true: self?.getContacts()
case false:
DispatchQueue.main.async {
self?.permissionsError = .userError
}
}
}

#Published Array is not updating

i'm currently struggling to fetch any changes from an published variable in SwiftUI. Most of the code is created after this tutorial on YouTube.
It's basically an app, that fetches cryptos from a firebase database. To avoid high server costs I want to update any changes of the coins to the database but not have an observer to lower the download rate.
What's the bug?
When I'm adding a coin to my favorites, it sends the data correctly to the database and updates the UI. However when I try to filter the coins the Coin-array switches back to it's previous state. I also added a breakpoint on the CoinCellViewModel(coin: coin)-Line but it only gets executed when I change the filterBy. Here's a little visualisation of the bug:
Repository
class CoinsRepository: ObservableObject {
#Published var coins = [Coin]()
var ref: DatabaseReference!
init() {
self.ref = Database.database().reference()
loadDatabase(ref)
}
func loadDatabase(_ ref: DatabaseReference) {
ref.child("coins").observeSingleEvent(of: .value) { snapshot in
guard let dictionaries = snapshot.value as? [String: Any] else { return }
var coinNames: [String] = []
self.coins = dictionaries.compactMap({ (key: String, value: Any) in
guard let dic = value as? [String: Any] else { return nil }
coinNames.append(dic["name"] as? String ?? "")
return Coin(dic)
})
}
}
func updateFavorite(_ coin: Coin, state: Bool) {
let path = ref.child("coins/\(coin.name)")
var flag = false
path.updateChildValues(["favorite": state]) { err, ref in
if let err = err {
print("ERROR: \(err.localizedDescription)")
} else {
var i = 0
var newCoinArray = self.coins
for coinA in newCoinArray {
if coinA.name == coin.name {
newCoinArray[i].favorite = state
}
i += 1
}
// I guess here's the error
DispatchQueue.main.async {
self.objectWillChange.send()
self.coins = newCoinArray
}
}
}
}
}
ViewModel
class CoinListViewModel: ObservableObject {
#Published var coinRepository = CoinsRepository()
#Published var coinCellViewModels = [CoinCellViewModel]()
#Published var filterBy: [Bool] = UserDefaults.standard.array(forKey: "filter") as? [Bool] ?? [false, false, false]
#Published var fbPrice: Double = 0.00
#Published var searchText: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.combineLatest(coinRepository.$coins, $fbPrice, $filterBy)
.map(filter)
.sink { coins in
self.coinCellViewModels = coins.map { coin in
CoinCellViewModel(coin: coin)
}
}
.store(in: &cancellables)
}
...
}
updateFavorite(_ coin: Coin, state: Bool) get's called in the CoinCellViewModel() but I guess the code isn't necessary here...
I'm fairly new to the Combine topic and not quite getting all the new methods, so any help is appreciated!

SwiftUI combine nil data

I have created a class to perform a network request and parse the data using Combine. I'm not entirely certain the code is correct, but it's working as of now (still learning the basics of Swift and basic networking tasks). My Widget has the correct data and is works until the data becomes nil. Unsure how to check if the data from my first publisher in my SwiftUI View is nil, the data seems to be valid even when there's no games showing.
My SwiftUI View
struct SimpleEntry: TimelineEntry {
let date: Date
public var model: CombineData?
let configuration: ConfigurationIntent
}
struct Some_WidgetEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily) var widgetFamily
var body: some View {
VStack (spacing: 0){
if entry.model?.schedule?.dates.first?.games == nil {
Text("No games Scheduled")
} else {
Text("Game is scheduled")
}
}
}
}
Combine
import Foundation
import WidgetKit
import Combine
// MARK: - Combine Attempt
class CombineData {
var schedule: Schedule?
var live: Live?
private var cancellables = Set<AnyCancellable>()
func fetchSchedule(_ teamID: Int, _ completion: #escaping (Live) -> Void) {
let url = URL(string: "https://statsapi.web.nhl.com/api/v1/schedule?teamId=\(teamID)")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Schedule.self, decoder: JSONDecoder())
//.catch { _ in Empty<Schedule, Error>() }
//.replaceError(with: Schedule(dates: []))
let publisher2 = publisher
.flatMap {
return self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
}
Publishers.Zip(publisher, publisher2)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in
}, receiveValue: { schedule, live in
self.schedule = schedule
self.live = live
completion(self.live!)
WidgetCenter.shared.reloadTimelines(ofKind: "NHL_Widget")
}).store(in: &cancellables)
}
func fetchLiveFeed(_ link: String) -> AnyPublisher<Live, Error /*Never if .catch error */> {
let url = URL(string: "https://statsapi.web.nhl.com\(link)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Live.self, decoder: JSONDecoder())
//.catch { _ in Empty<Live, Never>() }
.eraseToAnyPublisher()
}
}
Like I said in the comments, it's likely that the decode(type: Live.self, decoder: JSONDecoder()) returns an error because the URL that you're fetching from when link is nil doesn't return anything that can be decoded as Live.self.
So you need to handle that case somehow. For example, you can handle this by making the Live variable an optional, and returning nil when link is empty (or nil).
This is just to set you in the right direction - you'll need to work out the exact code yourself.
let publisher2 = publisher1
.flatMap {
self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
.map { $0 as Live? } // convert to an optional
.replaceError(with: nil)
}
Then in the sink, handle the nil:
.sink(receiveCompletion: {_ in }, receiveValue:
{ schedule, live in
if let live = live {
// normal treatment
self.schedule = schedule
self.live = live
//.. etc
} else {
// set a placeholder
}
})
SwiftUI and WidgetKit work differently. I needed to fetch data in getTimeline for my IntentTimelineProvider then add a completion handler for my TimelineEntry. Heavily modified my Combine data model. All credit goes to #EmilioPelaez for pointing me in the right direction, answer here.

How to auto update the UI in SwiftUI when changes are made in realm database?

My realm database structure looks like this example:
class Person: Object, Identifiable {
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var name: String = ""
var dogs = RealmSwift.List<Dog>()
}
class Dog: Object, Identifiable {
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var name: String = ""
let human = RealmSwift.LinkingObjects<Person>(fromType: Person.self, property: "dogs")
}
To simplify CRUD operations I have a database manager:
extension Realm {
public func safeWrite(_ block: (() throws -> Void)) throws {
if isInWriteTransaction {
try block()
} else {
try write(block)
}
}
}
class DatabaseManager {
private let realm: Realm
public static let sharedInstance = DatabaseManager()
private init(){
realm = try! Realm()
}
func save(_ obj: Object){
do {
try realm.safeWrite {
realm.add(obj, update: .all)
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func save(_ obj: Object,_ block: () -> Void){
do {
try realm.safeWrite{
realm.add(obj, update: .all)
block()
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func save(_ objs: [Object]){
do {
try realm.safeWrite {
realm.add(objs, update: .all)
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
func delete(_ obj: Object){
do {
try realm.safeWrite {
realm.delete(obj)
}
} catch {
NSLog("error deleting object: %#", [error])
}
}
func update(_ block: #escaping () -> Void){
do {
try realm.safeWrite{
block()
}
} catch {
NSLog("error updating object: %#", [error])
}
}
}
Now I have different views where I need to access my database and display some data. For this case I've created a view model class:
class PersonViewModel: ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var persons: Results<Person> = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
public func fetch(){
self.persons = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
}
public func addPerson(person: Person){
realm.save(Person)
self.fetch() // <---- necessary to update the UI: how to auto update?
}
}
This view model class will be passed as an #EnvironmentObject to the views where I need the data. As you can see I need to fetch after each database operation all data again to have a "fresh updated" Results<Person>. I know that Results<T> are live, but it has no effect to the UI. Is there any way to auto updating all views when I do a change on the database without fetching all data manually again?
While Realm automatically updates collections as new data is deleted, added or changed, there needs to be a trigger that notifies your code of those changes.
That's a notification aka observer.
In this case there's an Results object persons populated with the fetchData function:
#Published var persons: Results<Person> = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
So when a person is deleted, added or changed the persons results object will be automatically updated. However to be notified of that, an observer needs to be added, and there are two components: a class level notification token and then a function to handle the event.
A notification token is defined like this (note a strong reference is needed to keep it alive)
var notificationToken: NotificationToken? = nil
and ensure it's deallocated when the view closes
deinit {
self.notificationToken?.invalidate()
}
and then the code (in it's simplist form) that will execute when changes to people occurs
self.notificationToken = self.persons?.observe { changes in
switch changes {
case .initial:
print("initial load")
//the initial data is ready - reload tableviews/update ui etc
case .update(_, _, _, _):
print("update")
//some change occurred, reload tableviews/update ui etc
case .error(let error):
fatalError("\(error)")
}
}
Here's a link that covers this in depth Collection Notifications

How do you use the result of a function as a Bindable object in Swiftui?

I'm developing a simple SwiftUI app, using Xcode 11 beta5.
I have a list of Place, and i want to display the list, and add / edit them.
The data come from core data.
I have 3 classes for this :
- CoreDataController, which handle the connection to core data
- PlaceController, which handle operation on the Places.
public class CoreDataController {
static let instance = CoreDataController()
private let container = NSPersistentContainer(name: "RememberV2")
private init() {
print("Start Init DataController")
container.loadPersistentStores { (storeDescription, error) in
if let error = error {
fatalError("Failed to load store: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
print("End Init DataController")
}
func getContext() -> NSManagedObjectContext {
return container.viewContext
}
func save() {
print("Start Save context")
do{
try container.viewContext.save()
} catch {
print("ERROR - saving context")
}
print("End Save context")
}
}
public class PlaceController {
static let instance = PlaceController()
private let dc = CoreDataController.instance
private let entityName:String = "Place"
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
func createPlace(name:String) -> Bool {
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
dc.save()
DataController.instance.places = getAllPlaces()
return true
}
func createPlace(name:String, comment:String) -> Bool {
print("Start - create place with comment")
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
newPlace.setValue(comment, forKey: "comment")
dc.save()
print("End - create place with comment")
DataController.instance.places = getAllPlaces()
return true
}
func getAllPlaces() -> [Place] {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
if let fp = try? dc.getContext().fetch(r) as? [Place] {
return fp
}
return [Place]()
}
func truncatePlaces() -> Bool {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let batch = NSBatchDeleteRequest(fetchRequest: r)
if (try? dc.getContext().execute(batch)) != nil {
return true
}
return false
}
}
In my view i simply use the function :
List (pc.getAllPlaces(), id: \.id) { place in
NavigationLink(destination: PlaceDetail(place: place)) {
PlacesRow(place:place)
}
}
It works to display the information, but if i add a new place, the list is not updated.
I have to go back to the home screen, then display again the Places screen for the list to be updated.
So i use another controller :
class DataController: ObservableObject {
#Published var places:[Place] = []
static let instance = DataController()
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
}
In my view, i just display the ObservedObject places.
#ObservedObject var data: DataController = DataController.instance
And in my PlaceController, i update the table in the DataController
DataController.instance.places = getAllPlaces()
That works, but i have this warning :
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes
Also i'm pretty sure there is a better way to do this ...
Any idea what is this better way ?
Thanks,
Nicolas