Assets, Scripts

Accessing Assets

So, you’ve got a bunch of assets. Thats great! Xcode xcasset folders are a great way of managing your assets. And with Xcode 9, the addition of color assets makes it even better. However, there is a shortcoming. That shortcoming happens when you want to use an asset in code. To do so you need to do something like this:

let image = UIImage(named:"Image Asset Name")
let color = UIColor(named:"Color Asset Name")

Hmmm… This seems error prone. You could easily forget or mistype an assets name. This problem becomes worse the more assets you have. This seems like a perfect solution for enums.

Enums in Swift are fantastic (have I said that before?). They can have different raw types, functions, computed variables, etc. They are great! In this case we could create an enum with each assets name as a case:

enum Assets:String{
    case AssetName1
    case AssetName2
    ...
}

Then our above code would become:

let image = UIImage(named:Assets.ImageAssetName.rawValue)
let color = UIColor(named:Assets.ColorAssetName.rawValue)

This is a step up. It eliminates the need to remember/spell the asset names. We can do better! Lets extend our enum with a function that returns UIImage / UIColor objects:

extension Assets{
    var image:UIImage?{
        return UIImage(named:self.rawValue)
    }
    var color:UIColor?{
        return UIColor(named:self.rawValue)
    }
}

Or beginning code becomes:

let image = Assets.ImageAssetName.image
let color = Assets.ColorAssetName.color

This is an improvement, but we still have a couple of problems to tackle. First, our Assets enum makes no distinction between asset types. Because of this our extension to the Assets enum adds the image and color variables to all assets, wether they reference an image or not. The second problem we have is that we need to worry about adding/deleting enum cases every time we add/delete assets. This seems like an easy step to miss and not something pleasant to handle/remember. In fact it seems tedious and repetitive… Hmmmm…. Tedious and repetitive… That sounds like a job for a computer to handle!

Getting Scripty With It

Whenever I come to a situation where I need to do a job repetitively I try and script it. It gives me a chance to use python and usually solves more problems than it creates :P.

If you open up an xcasset folder you’ll find it contains subfolders; one for each asset. The folder name is the same as the asset name, plus an extension that indicates the asset type; .colorset for a color asset, .imageset for an image asset, etc. So to automate the creation of our enums, we simply need to traverse our project folders, finding folders with the .xcassets extension, then traversing their subfolders, pulling out their asset names.

While I could copy and paste the python script here, I’ve placed it into a GitHub repository instead:

https://github.com/DrCarleeknikov/XcodeAssetsScript/

Using this script is simple:

  1. Get the source code above.
  2. Copy AssetEnumCreator.py into your projects root directory.
  3. Add a Run Script Phase to your target’s Build Phases and copy python AssetEnumCreator.py like so:
  4. Build (⌘B). This runs the script and will create 3 files for every .xcassets directory. These files are created in the same directory as the .xcassets (not in the .xcassets folder). For example, if you assets directory is called Assets.xcassets then the script will create the following three files in the same directory containing Assets.xcassets:
    1. ColorAssets.swift (Contains an enum with that assets color assets)
    2. ImageAssets.swift (Contains an enum with that assets image assets)
    3. AssetsUIColorExtension.swift (Contains an extension to UIColor that allows you to access the color assets via the typical UIColor static methods: UIColor.myRed
  5. Finally, you need to add these files to your project. Simply locate them in finder and drag and drop them into your project.

With these steps done, you can add/edit assets inside you .xcassets directories and then when you build, the enums and UIColor extensions will be automatically updated! Note: if you add another .xcassets directory, the build will generate the .swift files, but you will need to complete step 5 before you can use them in code.

 

TL;DR

Use this repository to automatically create enums/class extensions for your project assets:

https://github.com/DrCarleeknikov/XcodeAssetsScript/

 

 

Scripts

Reporting For Duty

Who likes getting a daily report on the number of downloads they’ve had from iTunes? Me too! Thats why I recently wrote a python script to download those numbers and email me a copy of the results, every day! Woot woot! This tutorial is going to show you how to do it.

iTunesConnect and Reporter

If you go into your iTunesConnect account, and tap on the Sales and Trends icon, you will see the sales your applications have had over the last week or so. You can also see how many downloads were done on a given day. While this is all good, I don’t like to need to log into iTunesConnect every day to view these numbers.  I would really just like them to be sent to me… (Lazy! right?).  Luckily for me (and us, if you are still reading this post), Apple has provided a way for us to automate this process!

The first thing we need to do is to download their Reporter files (here). This will download a zip file, containing a folder called Reporter.  This folder contains two files, Reporter.jar, and Reporter.properties.  These files allow us to access and download a report from iTunesConnect.  So we need to move this folder to wherever you would like these files to be saved. I simply put the folder in my documents folder… Seemed logical…

Great! Now, the Reporter.jar file requires you to have Java 1.6 or later installed.  So make sure that you are up to date on that front. (If you need to download Java, you can do so here). With that bit of house keeping done, we can start having fun!

If you open up your Reporter.properties it should look something like this:

AccessToken=

Mode=Normal

SalesUrl=https://reportingitc-reporter.apple.com/reportservice/sales/v1
FinanceUrl=https://reportingitc-reporter.apple.com/reportservice/finance/v1

So we see that we need to provide an AccessToken… The rest doesn’t really concern us. But we do need to find that access token.

There are two ways to do this. Perhaps the easiest is via iTunesConnect. To do so, follow these steps:

1) From the homepage, click Sales and Trends.

2) In the upper-left corner, click the pop-up menu and choose Reports.

3) In the upper-right corner, click on the tooltip next to About Reports.

4) Click Generate Access Token.

5) Copy the access token to your clipboard.

6) Paste the access token to the RHS of the equals:
       AccessToken=#YourTokenHere#

Much of the above instructions comes form Apples help document on using reporter. You can read it here for more information (like how to get your token via command line).

At this point, you should be able to start using Reporter. The first thing we need to do is to get the account number your want to download your numbers from.  To do so, open your terminal app of choice, navigate to the Reporter folder, and run the following command:

java -jar Reporter.jar p=Reporter.properties Sales.getAccounts

This should print out a list of companies, or iTunesConnect accounts your have access to, followed by a number:

Company1 LLC, 123456
Company2 LLC, 234567
...

That account number is what we need!

Next we need to get the vendor number we need. Run the following command in terminal:

java -jar Reporter.jar p=Reporter.properties a=YOUR_ACCOUNT_NUMBER Sales.getVendors

This should print out a list of vender numbers. Now, for me it only showed one number, so I’m not really sure what the difference each vendor number makes… So, if you had more than one vendor number, you may need alter the script to iterate over the vendor numbers… But now we have everything we need to write our python script!

Python Script

Open up your favorite python IDE and create a new file called Reporter.py.  Copy the following code into this new file and change out the base_file_path, account_number, and vendor_number values to the values you collected above:

# set current directory to
import os
base_file_path = "Path To your Reporter file"
os.system("cd " + base_file_path)

# get a string version of yesterdays date
from datetime import date, timedelta

yesterday = date.today() - timedelta(1)
yesterday_str = yesterday.strftime("%Y%m%d")

# request report from itc
import subprocess
account_number = "Your Account Number"
vendor_number = "Your Vender Number"
output = subprocess.check_output("java -jar Reporter.jar p=Reporter.properties a=" + account_number +
                                 " Sales.getReport " + vendor_number + ", Sales, Summary, Daily, " + yesterday_str,
                                 shell=True)

These steps are all pretty clear, we set the current directory to the directory containing the Reporter.jar and Reporter.properties files. Next, we calculate yesterdays date and transform that date to a string. Finally we request from iTunesConnect a report containing the sales information for yesterday.

If you run this code, you should get a .gz file downloaded into your Reporter folder.  Double click this and you will get a .txt file.  This .txt file contains all the sales information from yesterday. Looking over this information, you might notice multiple lines for the same app. Not quite what we wanted (though I do like summing things up in my head…). So our next step is to parse this document and count the different types if sales each application had. You see, the totals listed are divided by sales type (download, redownload, update, etc) as well as the country it was sold in. As I was only interested in the type of sale, not the country, the following script combines by country, but separates by type. Lets have a look, shall we? Copy the following code below your last line in your Reporter.py file:

# open downloaded itc report file and unzip it.
import gzip
zip_filename = str(output.decode('UTF-8').split()[-1])
zip_ref = gzip.open(base_file_path + zip_filename, 'r')
file_contents = zip_ref.read().decode("UTF-8").split('\n')
zip_ref.close()

# parse contents
# get header row, find desired indices
headers = file_contents[0].split('\t')
title_index = headers.index('Title')
units_index = headers.index('Units')
download_type_index = headers.index('Product Type Identifier')

# iterate through data rows, getting desired data.
new_download_types = ["1", "1E", "1EP", "1EU", "1F", "1T", "F1"]
redownload_types = ["3", "3F", "3T", "F3"]
update_types = ["7", "7F", "F7"]
iap_types = ["IA1-M", "IA1", "IA9", "IA9-M", "IAC", "IAC-M", "IAY", "IAY-M", "FI1"]

# drop header row
file_contents = file_contents[1:]

So, the first thing we do, is unzip the file and open it.  Next, we find the indices for the columns we are interested in.  In our case we are only interested in the Title (product name), Units (number of sales), and Product Type Identifier. The Product Type Identifier is an identification code that indicates what type of sale happened.  As the next section shows, for example, 1, 1E, 1EP, 1EU, 1F, 1T, and F1 all indicate a new download.  Each number indicates a different type of new download, such as Mac, iPad, iPhone, Universal, etc. But again, I’m only interested in a total, so I am throwing them into the same basket. Note that iap_types stands for in-app-purchas types.  Next, we drop the header file, since we are done with it.

Awesome! Now paste the following code below the above code (Warning! this should be simplified… it was late and I wanted to get this over with, so I was a little too… copy-paste happy…), :

new_downloads = dict()
redownloads = dict()
updates = dict()
iaps = dict()
for row in file_contents:
    row_array = row.split("\t")
    if len(row_array) < max(download_type_index, title_index, units_index):
        break

    product_title = row_array[title_index]
    units = int(row_array[units_index])

    download_type = row_array[download_type_index]

    # Downloads
    if download_type in new_download_types:
        if product_title in new_downloads:
            previous_total = new_downloads[product_title]
            new_downloads[product_title] = previous_total + units
        else:
            new_downloads[product_title] = units

    # redownloads
    if download_type in redownload_types:
        if product_title in redownloads:
            previous_total = redownloads[product_title]
            redownloads[product_title] = previous_total + units
        else:
            redownloads[product_title] = units

    # Downloads
    if download_type in update_types:
        if product_title in new_downloads:
            previous_total = updates[product_title]
            updates[product_title] = previous_total + units
        else:
            updates[product_title] = units

    # Downloads
    if download_type in new_download_types:

        if product_title in iap_types:
            previous_total = iaps[product_title]
            iaps[product_title] = previous_total + units
        else:
            iaps[product_title] = units

Basically, we are iterating over each row of the downloaded report and checking its type and adding the unit to the total being stored in the dictionaries created at the top of the code snippet… Thus we will have a dictionary containing the ProductName as the keys, and the total sales for that type (new downloads, updates, etc) as the value.

Tubular, now we have all the information condensed in the format we want.  Next we need to email it to ourselves. First we create the message text in HTML from the dictionaries we just created.  Again, I was lazy and simply copy and pasted my way through this:

# create email
message = ''  # "<strong>New Downloads</strong><b />"
message += '<p><table style="width:100%"><caption>New Downloads</caption><tr><th>Name</th><th>Units</th></tr>'
for key, value in new_downloads.items():
    message += '<tr><td>' + key + "</td><td>" + str(value) + '</td></tr>'

message += "</table></p><b /><b />"

# message += "<strong>New In App Purchases</strong><b />"
message += '<p><table style="width:100%"><caption>New In App Purchases</caption><tr><th>Name</th><th>Units</th></tr>'
for key, value in iaps.items():
    message += '<tr><td>' + key + "</td><td>" + str(value) + '</td></tr>'

message += "</table></p><b /><b />"

# message += "<strong>Updates</strong><b />"
message += '<p><table style="width:100%"><caption>Updates</caption><tr><th>Name</th><th>Units</th></tr>'
for key, value in updates.items():
    message += '<tr><td>' + key + "</td><td>" + str(value) + '</td></tr>'

message += "</table></p><b /><b />"

# message += "<strong>Redownloads</strong><b />"
message += '<p><table style="width:100%"><caption>Redownloads</caption><tr><th>Name</th><th>Units</th></tr>'
for key, value in redownloads.items():
    message += '<tr><td>' + key + "</td><td>" + str(value) + '</td></tr>'

message += "</table></p><b /><b />"

Now that we have the message text created, lets create the email:

# skipped your comments for readability
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

me = "YOUR EMAIL"
my_password = r"YOUR EMAIL PASSWORD"
you = "YOUR EMAIL... AGAIN"

msg = MIMEMultipart('alternative')
msg['Subject'] = "Yesterday's Downloads!"
msg['From'] = me
msg['To'] = you

html = '<html><head><style>table { font-family: arial, sans-serif; border-collapse: collapse; width: 100%;}' \
       'td, th {border: 1px solid #dddddd;text-align: left;padding: 8px;}' \
       'tr:nth-child(even) {background-color: #dddddd;}' \
       '</style></head><body>' + message + '</body></html>'
part2 = MIMEText(html, 'html')

msg.attach(part2)

# Send the message via gmail's regular server, over SSL - passwords are being sent, afterall
s = smtplib.SMTP_SSL('smtp.gmail.com', 465)
# uncomment if interested in the actual smtp conversation
# s.set_debuglevel(1)
# do the smtp auth; sends ehlo if it hasn't been sent already
s.login(me, my_password)

s.sendmail(me, you, msg.as_string())
s.quit()

First, this is configured to send an email via a gmail account.  It may differ depending on your client.  Next, I got this after exploring several different SO answers on sending emails from python, but I can’t seem to find it again. (If you know where this is, comment and I’ll link it…).

OK, now that I’ve said that, if you add the info that you need to (email and password info) and run the code you will probably get an error indicating that google will not allow the request. You will probably also get an email from google stating that they blocked a log-in.  To get it to work, you need to click on the link in the email to allow less-secure apps to log into your account.

Once you’ve done that, you should be able to run the code and get an email with all your numbers in a pretty HTML table! Woot woot!

Fun, huh? The only thing left to do, is to automate this script so that it runs every day. I used an app called Plisterine. Simply download and install this application.  Then, to run our python script, set it to the file selected in the Application/script to launch section, and configure the rest of the options to your liking.  I have my launching on a schedule, every day at 11:00 a.m. Hit continue, follow the prompts and, you have got daily download emails coming your way!

TL;DR
  1. Download and install reporter.
  2. Download Reporter.py file, and move into your Reporter folder.
  3. Fill out the missing information in the Reporter.py file.
  4. (Optional) Automate the running of the Reporter.py file using Plisterine.
Sources

http://help.apple.com/itc/contentreporterguide/en.lproj/static.html#apda86f89da5

http://www.launchd.info