@ccc I'm up for that. I'm pretty weak on objective-c so my code is a little scratchy. I would benefit from someone giving it the once over.
Do you think I should fork it, or create a branch and merge back in?
Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
@ccc I'm up for that. I'm pretty weak on objective-c so my code is a little scratchy. I would benefit from someone giving it the once over.
Do you think I should fork it, or create a branch and merge back in?
Hi Matteo
Sure, he goes:
The entire game engine was written, tested and debugged in Pythonista on my iPhone. I combined this code with the Pythonista App Template (https://github.com/omz/PythonistaAppTemplate). The app template contains a version of Pythonista as a library which run your python code within Xcode. I had to do a small amount of debugging in Xcode to cope with some small differences between the version of pythonista in the template (which seems a little old) and the latest version of pythonista. I also extended the template a little to accommodate in-app purchases.
Yes you definitely need a mac to upload to the App store. There is no way round this as this is the way apple have designed it. There are ways to distribute python code to other pythonista users by making your code open source. An example is stash (shell for pythonista) see https://github.com/ywangd/stash. Stash is installed by cutting and pasting some simple commands into pythonista.
Pythonista apps are probably always going to be a little bit on the big side. The pythonistakit library itself is 50mb. There is also a feature of the pythonista template which copies your code and files to a writable directory (in case you code needs to change them). This has the effect of roughly doubling the size of your app (this is the documents and data that you allude to). In my case most of the 100Mb is music files as MapMan has about 40minutes of original music, the rest is images and a very small amount is the actual source code. I think a bare bones pythonista yellow world app would be about 50-60mb. The pythonista template itself contains a hello world example.
I think my pure python code is very small i.e. well under 1Mb. The compile pythonistakit is around 50Mb. There are also various python modules that are bundled as standard in the template, but these can be deleted if your app doesn't use them.
Note also there are a few pitfalls with the template owing to Apple's submission policy, I would in particular flag this discussion on the forum: https://forum.omz-software.com/topic/3744/xcode-template-for-pythonista/25?page=2. I got it working, but it did take a bit of fiddling.
Cheers
Peter
Quick update after two days in the App Store we’ve had 9342 downloads, not bad for a game written on a phone!
You can see the sales chart here https://m.facebook.com/mapmangame/
or here
https://twitter.com/mapmangame/status/993376703765532673?s=20
No problem, I will put something together. Maybe a blog or something spelling it out. I’m catching up with a few things after getting the MapMan release out, so it might be a few days.
In the mean time the outline answer is that I added objective-c code to the template to handle the purchases and then used objects_util calls within my python script to call out the the objective-c code. As the Apple store kit is asynchronous I defined a callback interface within the template which I implemented in the python code. So I could continue to code my app within Pythonista I used dependency injection to switch between the real purchase class (used when my game was launched from the template)
and dummy purchase class (used when launching within Pythonista). I guess the template could be extended with the code I wrote (it would need a little cleaning up as I have a very poor grasp of objective-c).
The resorted to the above as I could not get in-App working with just objc_util calls within Pythonista. I think this is down to Apple’s security around in-App purchases which forces them to original from the main thread (but I’m guessing).
Last post on this, I promise!
MapMan is now available for download in the App Store!
MapMan is free to download and offers an optional in-app purchase (the fist Pythonista App to do this - as far as I know!).
For more info on the game please see http://mapmangame.com and my blog telling the storing of how it was developed on my iPhone http://mapmangame.blogspot.co.uk
It you are on twitter we are @mapmangame and any retweets would be really appreciated.
Thanks again to the Pythonista Forum for all the help over the last year, this is a really great community!
Thansk finally to @omz for creating a truly remarkable app and opening up the possibility of developing real iPhone Apps on an iPhone! It's been fun!
Hello, me again!
MapMan is now available for pre-order on the App Store. If you pre-order now and you all get it as soon as its released on May 4th. It's free with an optional In-App purchase.
It's great to see it finally showing in the App Store. Thanks again to the Pythonista forum for helping me out many times when developing this!
Thanks
Peter
Hi
You could try writing a web app in Flask within pythonista. I believe there are a few tools that let you turn flask apps into standalone desktop apps (although I haven't tried them myself) e.g. https://github.com/Widdershin/flask-desktop
Maybe someone on the forum has experience of this strategy? I would be interested to try it myself sometime.
Hi Everyone
Further to my post a copy of weeks back we have set a release date for MapMan for Friday the 4th of May.
MapMan was coded using Pythonista and built into an App using the Pythonista App Template. It was a really fun project and I really hope it showcases just what's possible with Pythonista.
You can read more about how MapMan was developed by reading our blog at http://mapmangame.blogspot.co.uk
We also have a website mapmangame.com, a Facebook page and we are @mapmangame on twitter.
.
I'll post again on Friday to announce the release. If you have a few minutes it would be really appreciated if you could download and try out MapMan. It's free to download with an optional in-app purchase.
Thanks!
Peter
I have got this working by moving the code that uses storekit into the pythonista template. objc_util calls from within pythonista are used to control the code in the template.
I think the security around storekit might be conflicting with running the store kit calls from the python interpreter.
well both.
obj_response.products() is an empty array.
there are a number of articles out there that explain that an empty array denotes something is wrong.
quick update: I've been having some success shifting the purchase code into the Xcode App Template and then using objc_util to call the purchase code added to the template from within pythonista. I've managed to get the list of products back and I'm moving on to implementing purchases now.
Hi
I'm developing an app using pythonista and the pythonista app template. I've hit a snag trying to implement in app purchases using the code below (which is adapted from objective code here https://www.tutorialspoint.com/ios/ios_in_app_purchase.htm).
The issue is that the receive_products method, which does get called-back, is returned an empty list of products. I have checked the itunes connect set up and it seems fine.
To investigate the issue further I added some objectives code to the pythonista app template and managed to successfully return the correct list of products. This is not useful in practice as I want the user to be able to buy the products after the main.py python script has been started.
I therefore think the issue relates to threads i.e. I that that store kit probably needs the code to run on the main thread, but I'm not sure. I've tried adding the @on_main_thread decorator, but it's not working.
Does anyone have any suggestions? Any help really appreciated (as always)
from objc_util import *
import ctypes
import time
def fetchAvailableProducts(_self, _cmd):
obj = ObjCInstance(_self)
sk_class = ObjCClass("SKProductsRequest")
InApp.Instance.products_request = sk_class.alloc().init(productIdentifiers=ns(InApp.PRODUCTS))
InApp.Instance.products_request.delegate = obj
InApp.Instance.products_request.start()
def productsRequest_didReceiveResponse_(_self, _cmd, request, response):
#defined here: https://developer.apple.com/documentation/storekit/skproductsrequestdelegate/1506070-productsrequest
InApp.Instance.receive_products(response)
def paymentQueue_updatedTransactions_(_self, _cmd, queue, transactions):
InApp.Instance.update_transactions(queue, transactions)
class Product:
def __init__(self, identifier, title, description, price):
self.identifier = identifier
self.title = title
self.description = description
self.price = price
class InApp:
PRODUCTS = ['com.YYY.ZZZ']
Instance = None
@classmethod
def initialize(cls):
cls.Instance = InApp()
cls.Instance.fetch()
@classmethod
def initialize_dummy(cls):
cls.Instance = InAppDummy()
def log(self, message):
self.log.append(message)
def update_successfull_purchase(self, product_identifier):
for observer in self.observers:
observer.purchase_successful(product_identifier)
def update_failed_purchase(self, product_identifier):
for observer in self.observers:
observer.purchase_failed(product_identifier)
def update_restored_purchase(self, product_identifier):
for observer in self.observers:
observer.purchase_restored(product_identifier)
def get_valid_product(self, product_identifier):
for product in self.valid_products:
if product.product_identifier == product_identifier:
return product
raise Exception('Product is not valid: {0}'.format(product_identifier))
def is_valid_product(self, product_identifier):
for product in self.valid_products:
if product.product_identifier == product_identifier:
return True
return False
@on_main_thread
def purchase(self, product_identifier):
if not self.can_make_purchases:
raise Exception('Purchases are disabled')
else:
product = self.get_valid_product(product_identifier)
sk_payment_class = ObjCClass("SKPayment")
payment = sk_payment_class.alloc().init(product=product)
default_queue = ObjCClass("SKPaymentQueue").defaultQueue
default_queue.addTransactionObserver(self.purchase_controller)
default_queue.addPayment(sk_payment_queue_class)
@on_main_thread
def update_transactions(self, queue, transactions):
for transaction in ObjCInstance(transactions):
transaction_state = transaction.transactionState
if transaction_state == "SKPaymentTransactionStatePurchasing":
self.log('Purchasing')
elif transaction_state == "SKPaymentTransactionStatePurchased":
if transaction.payment.productIdentifier in InApp.PRODUCTS:
self.log('Purchased')
self.update_successfull_purchase(transaction.payment.productIdentifier)
default_queue = ObjCClass("SKPaymentQueue").defaultQueue
default_queue.finishTransaction(transaction)
elif transaction_state == "SKPaymentTransactionStateRestored":
self.log('Restored')
self.update_restored_purchase(transaction.payment.productIdentifier)
default_queue = ObjCClass("SKPaymentQueue").defaultQueue
default_queue.finishTransaction(transaction)
elif transaction_state == "SKPaymentTransactionStateFailed":
self.update_failed_purchase(transaction.payment.productIdentifier)
self.log('Failed')
@on_main_thread
def receive_products(self, response):
self.products_validated = True
self.valid_products = []
self.invalid_products = []
obj_response = ObjCInstance(response)
valid_products = ObjCInstance(obj_response.products())
self.valid_count = len(valid_products)
for valid_product in valid_products:
print valid_product.productIdentifier
if (valid_product.productIdentifier in InApp.PRODUCTS):
product = Product(valid_product.productIdentifier,
valid_product.localizedTitle,
valid_product.localizedDescription,
valid_product.price)
self.valid_products.append(product)
for invalid in obj_response.invalidProductIdentifiers():
self.invalid_products.append(invalid)
def __init__(self):
self.products_request = None
self.observers = []
self.log = []
self.products = []
self.products_validated = False
self.valid_count = 0
self.invalid_products = []
def add_observer(self, observer):
self.observers.append(observer)
@on_main_thread
def fetch(self):
self.check_purchases_enabled()
ObjCClass('NSBundle').bundleWithPath_('/System/Library/Frameworks/StoreKit.framework').load()
superclass = ObjCClass("NSObject")
methods = [fetchAvailableProducts,
productsRequest_didReceiveResponse_,
paymentQueue_updatedTransactions_]
protocols = ['SKProductsRequestDelegate', 'SKPaymentTransactionObserver']
purchase_controller_class = create_objc_class('PurchaseController', superclass, methods=methods, protocols=protocols)
self.purchase_controller = purchase_controller_class.alloc().init()
self.purchase_controller.fetchAvailableProducts()
@on_main_thread
def check_purchases_enabled(self):
sk_payment_queue_class = ObjCClass("SKPaymentQueue")
self.can_make_purchases = sk_payment_queue_class.canMakePayments()