Category Archives: Programming

Multiple UIDynamicAnimators

In past apps, I tended to have one UIDynamicAnimator in my ViewController and that was that. UIDynamicAnimator allows you to use UIDynamics / effects on your UIViews.

The issue that I ran into was that removeBehavior(_:), which “Removes a specified dynamic behavior from a dynamic animator“, didn’t seem to work. I would keep track of specific UIDynamicBehavior instances and pass them as the argument for removeBehavior(_:) but it didn’t appear to remove the behavior.

What does work is calling removeAllBehaviors() on the UIDynamicAnimator. This is fine if you only have one UIView. But most likely, you have multiple UIViews & behaviors. Calling remove all on the only animator isn’t a good idea. That could leave UIViews frozen out of place.

Recently, I released a fun weekend app, Fun Faces. Browsing stack overflow, it occurred to me to use multiple UIDynamicAnimators. One for each UIView I wanted to animate. This worked for my use case, where calling removeAllBehaviors() doesn’t interrupt the other UIView’s behaviors (if any).

Using multiple UIDynamicAnimators isn’t an answer if you have multiple UIViews under the same animator with UICollisionBehavior or other effects that let the UIViews interact with each other.

Using CoreMotion deviceMotion to keep image level example (Xcode 8.3, Swift 3.1)

I’ve been playing around with CoreMotion since it is frankly so cool. I’ve followed NSHipster’s CMDevice​Motion post, but I made some changes to use the latest Swift v3.1. Below is sample code for using both the gyroscope and accelerometer to keep an image level when you rotate your phone.

//
//  ViewController.swift
//
//  Created by Rex on 4/22/17.
//

import UIKit
import CoreMotion

class ViewController: UIViewController {

    let interval = 0.01
    let imageFilename = "bg.jpg"
    let imageWidth = CGFloat(800)
    let imageHeight = CGFloat(1200)
    
    let manager = CMMotionManager()
    var imageView: UIImageView?

    override func viewDidLoad() {
        super.viewDidLoad()

        guard manager.isDeviceMotionAvailable else { return }
        
        setImageView()
        
        manager.deviceMotionUpdateInterval = interval
        let queue = OperationQueue()
        
        manager.startDeviceMotionUpdates(to: queue, withHandler: {(data, error) in
            guard let data = data else { return }
            let gravity = data.gravity
            let rotation = atan2(gravity.x, gravity.y) - .pi

            OperationQueue.main.addOperation {
                self.imageView?.transform = CGAffineTransform(rotationAngle: CGFloat(rotation))
            }
        })
    }
    
    func setImageView() {
        if let img = UIImage(named: imageFilename) {
            let iv = UIImageView(image: img)

            // center the image
            let x = (self.view.frame.width/2)-(imageWidth/2)
            let y = (self.view.frame.height/2)-(imageHeight/2)
            iv.frame = CGRect(x: x, y: y, width: imageWidth, height: imageHeight)
            
            self.view.addSubview(iv)
            self.imageView = iv
        }
    }
    
}

The setup is simple. Create a new Single View Application project in Xcode. You’ll need to add a JPG to the Assets.xcassets folder in the project. Replace the Viewcontroller with the code below and make sure to update the image filename, width, and height constants.

The code hopefully is straightforward. We make sure the CMMotionManager’s device motion is available. Then, we add the imageview (as the only UIView element we’re adding to the screen). We use an OperationQueue to process the rotation calculation off the main queue. Then we update the imageview with a transform on the main queue.

iOS 10 Locales and Currency Symbols – Sample App

While working on adding localization to my tip calculator, one thing that seems obvious in retrospect is the difference between a device’s language & region. iOS lets you set the language & region separately. For example, you might want to read text in English, but you could be in Asia. This is relevant to tipping since you could travel to a country where tipping is expected, but the country your phone’s language is associated with doesn’t traditionally tip.

While exploring locales and currency symbols, I whipped together a basic demo app that lets you scroll between all the known locales and their currency symbol in iOS 10. This is pretty useful since you can quickly see what the currencySymbol is for each known iOS locale.

Below is the full implementation of very hacked together (quick and dirty) code. All you need to do:

  • Create new Single View Application project in Xcode
  • Replace the ViewController.swift with below (written for Swift 3)
  • Run the app in Xcode
import UIKit

class ViewController: UIViewController, UITableViewDelegate {
    
    let cellIdentifier = "Cell"
    let currentLocaleHeight = CGFloat(80)
    
    let locales = Locale.availableIdentifiers.sorted { $0.localizedCaseInsensitiveCompare($1) == ComparisonResult.orderedAscending }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tableView: UITableView = UITableView()
        tableView.frame = CGRect(x: 0, y: currentLocaleHeight, width: view.frame.width, height: view.frame.height)
        tableView.dataSource = self
        tableView.delegate = self
        
        self.view.addSubview(tableView)
        
        addCurrentLocaleLabel()
    }
    
    func addCurrentLocaleLabel() {
        let local = Locale.current.identifier
        
        let width = view.frame.width
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: currentLocaleHeight))
        label.text = "Current locale: " + local
        label.textAlignment = .center
        view.addSubview(label)
    }
    
}

extension ViewController: UITableViewDataSource {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return locales.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .value1, reuseIdentifier: cellIdentifier)
        
        let localeString = locales[indexPath.row]
        
        let numberFormatter = NumberFormatter()
        numberFormatter.locale = Locale(identifier: localeString)
        
        cell.textLabel?.text = localeString
        cell.detailTextLabel?.text = numberFormatter.currencySymbol
        
        return cell
    }

}

Note that the “¤” symbol means the currency is unspecified.

Using Fastlane Snapshot to generate screenshots with UIPickerViews

This week, I released an update for my Tip Solver calculator to add Chinese localization. I had to generate 5 screenshots for 5 devices (iPhone and iPad) across 3 languages. In the time that I spent automating the process with Fastlane Snapshot, I could have easily done it manually in way less time. But the good news is that I’ve set myself up to painlessly generate screenshots for new languages. Snapshot takes some time to run, but it’s still a huge improvement over generating screenshots manually.

It took me a lot longer than I would have liked to setup my Snapshot process due to my usage of UIPickerViews. Tip Solver makes heavy usage of UIPickerView and I ran into many issues with UITest.

Your mileage may vary, but I found I had to do the following to be able to use UITest and UIPickerViews:

  • disable Ads (which run over the network)
  • drastically reduce the number of UIPickerView rows (in numberOfRowsInComponent)
  • use titleForRow instead of viewForRow for UITest running

The last one (using titleForRow) was a complete non starter since I rely on heavy UIPickerView visual customization. Generating screenshots with incorrect picker views defeats the whole point of the exercise.

I tried using the Xcode’s UITest recorder, but I ran into many issues. One glaring issue is that while recording, I was able to swipe the UIPicker up, but when I played it back, it ended up swiping up the Control Center (instead of adjusting the UIPicker). There is a method (adjustToPickerWheelValue), but I found that it only works with titleForRow (which I don’t use). What I would like is an expansion of the XCUIElement API to add a simple increment/move up or down once.

My final solution (aka work around) was to use a combination of Fastlane launch arguments & brute forcing the UIView (via UIViewController viewDidAppear) to generate my screenshots. My work around isn’t ideal, but it gets the job done.

In my Fastlane Snapfile, I was able to define arguments:

launch_arguments([
 "-screenshot 1",
 "-screenshot 2",
 "-screenshot 3",
 "-screenshot 4",
 "-screenshot 5"
])

In my ViewController (running Swift 3), I was able to handle them accordingly:

let screenshot = UserDefaults.standard.string(forKey: "screenshot")
if screenshot == "1" {
    // do something
} else if screenshot == "2" {
    // do something
} else if screenshot == "3" {
    // do something
} else if screenshot == "4" {
    // do something
} else if screenshot == "5" {
    // do something
}

Once everything is setup, generating screenshots was simply running snapshot on the command line.

I’m sure there’s room for improvement in the code (using an enum, etc.), but I left it at that since it’s only for screenshot generation.

If you’ve made it all the way down here, thanks for reading. I just wanted to share my experience with UITest and UIPickers. UITest probably needs more love from Apple as it was not pleasant to work with.

 

How to open phone app from iMessage extension and pass data

This weekend, I spent some time digging into iOS 10 iMessage app extensions.

Creating an iMessage app or sticker pack is relatively easy. Here are a couple of resources for getting started: tutsplus and medium. Apple also has their own example iMessage app.

For a new iMessage only app, you can choose File > New > Project and either ‘Sticker Pack Application’ or ‘iMessage Application’ in Xcode. To add an iMessage extension to your existing containing phone app, you can use File > New > Target and either ‘Sticker Pack Extension’ or ‘iMessage Extension’.

I wanted to understand the current state of user workflow between the iOS (main/phone) app and the iMessage extension app. Apple has it’s work cut out for them. The iMessage App store is an awkward modal triggered from an individual iMessage conversation. Users seem to have trouble locating the store and managing iMessage apps, so apps are getting bad reviews.

The good news is that you can launch your phone app from the iMessage app.

From the iMessage extension app, you can use this (with your own AppName):

guard let url: URL = URL(string: "AppName://?myParam=myValue") else { return }

self.extensionContext?.open(url, completionHandler: { (success: Bool) in
 
 })

Calling the above code will open the phone app. One issue I ran into (under the extension scheme) is that the phone app will crash in the simulator when opened this way. Xcode shows a SIGKILL since the iMessage app connected to Xcode is quit while the phone app is being opened. This appears to only be a simulator issue.

You can access the URL params in your phone app’s App Delegate:

    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {

        return true

    }

The bad news is that you cannot launch your iMessage app from your phone app. Apple must have been short on time, as their usual modus operandi is to launch headline features that are incomplete and hopefully iterate later if their business still cares. Radar(s) have been filed.

When asked, “Is it possible for my app to open the Messages app with my iMessage extension activated?” An Apple Staff member says, “There currently isn’t a way to do this.”

I think opening your iMessage app from your phone app would be an excellent, natural use case. User does something in the containing app, then they want to share that app’s state with a friend using iMessage. Why rely on the user going to Messages, finding your iMessage app in the correct convo, and then recreating app state to send it over?

So to recap, iMessage apps have a lot of potential as they are living apps within iMessages. They can communicate with their containing app. But I have reservations about the UX, including discoverability.

Use NSLocalizedString for user facing strings

Just a quick iOS tip I wish I knew earlier. Would’ve saved me some time & headache:

Always wrap user-facing strings with NSLocalizedString.

Even if you don’t plan to localize your app into any other languages, there is immense utility in being able to easily review all of the strings that a user will see. And if localization is in the cards, it’s significantly easier to NSLocalize your strings as you go along the first time, then try to find all of them after-the-fact.

via http://nshipster.com/nslocalizedstring/

The advice is sound. It won’t cost you any time to use NSLocalizedString the first time, but it will help save time if/when you localize.

Visual iOS guide on switching from Chinese to English

I wanted to document for my own future reference how to switch an iPhone (in the iOS Simulator) back to English from Chinese. My Chinese literacy is not very high, so I’m sure this guide will be handy.

Note: follow the red ovals and you will be able to change your iOS language back to English

1

2

3

4

5

6

7

8

Why would I need to do this? I’m working on language localization for different apps, and testing localization in the simulator is often easier than on actual hardware.

iOS Universal iPad Portrait Gotcha

I wanted to share a small tip that reinforces the necessity of on-device (non-simulator) testing.

While finalizing my latest iOS app (universal for both iPhone & iPad), I found an issue through manual QA on an actual iPad. I had only left Portrait checked in the Project > General section of Xcode, but my app was somehow running in landscape mode on the iPad.

general

Confused as to why it was rendering in both landscape & portrait mode on my iPad, I found a handy stack overflow post.

For one reason or another, you have to update your Info.plist to only specify portrait settings for iPads. Below is my Info.plist after I updated it to only target Portrait mode.

info_plist

It’s confusing as to why the Project General section’s Device Orientation is not sufficient to force only Portrait orientations and you have to also update the Info.plist.

Through simulator testing, it’s not likely that I would have caught onto this portrait vs landscape issue. I relied mostly on my primary iPhone and copious amounts of simulator testing for the other iOS universal devices.

For the highest level of quality control, you would need an iPhone 4s, iPhone 5, iPhone 6, iPhone 6+, iPad, and iPad Pro. That’s a lot of devices and I certainly don’t have all of those. Sidenote: if you do have all those devices, you would also be positioned to record App Preview videos for all devices natively (AKA lots of work).

Freeing up disk space as an iOS Developer

As an iOS Developer, I often have trouble updating to the latest version of Xcode since my 128GB MacBook Air keeps filling up.

Common culprits include ~/Library/Developer/Xcode/DerivedData and ~/Library/Developer/Xcode/iOS DeviceSupport.

By clearing out those two folders, I’m able to free up 11GB at this time of writing. For more information on what these folders contain, this was helpful.

Dynamic Infinite UIPickerView Scrolling Example

In iOS, I’ve seen a lot of examples of creating a UIPickerView with a hardcoded array of strings. This may be necessary if your data set is a list of strings (such as USA states). For numbers, there’s a much easier way to dynamically generate each row’s title.

To start off with, here’s a simple, programmatic UIPickerView implementation that includes the numbers (as strings) 0 through 9:

import UIKit

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupPicker()
    }

    var pickerView: UIPickerView = UIPickerView()
    var pickerDataSource = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
    
    func setupPicker() {
        pickerView.dataSource = self
        pickerView.delegate = self
        
        pickerView.frame = CGRectMake(0, 0, view.frame.width, 300)
        view.addSubview(pickerView)
    }
    
    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return pickerDataSource[row]
    }
    
    // columns count
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }
    
    // rows count
    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return pickerDataSource.count
    }
}

The key piece is the pickerDataSource that gives us our data source. It is an array literal, and in a worst case programming scenario, one can imagine a very, very long hardcoded array of looping “0” – “9”. A long hardcoded array works, but is not optimal for many reasons.

The code above looks like this in the simulator:

manual_ss

We can improve and change this in many ways. We are going to update the code to operate without using a hardcoded array literal. Also, the user has to scroll up (or down) for a very, very long time before they hit the end of the picker. When their scrolling stops, we will move the position of the selected row back to the middle, so that when they scroll again, there is a long way to go before they get to the top (or bottom).

import UIKit

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
    
    var pickerView: UIPickerView = UIPickerView()
    let pickerDataSize = 100_000
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupPicker()
    }
    
    func setupPicker() {
        pickerView.dataSource = self
        pickerView.delegate = self
        
        pickerView.frame = CGRectMake(0, 0, view.frame.width, 300)
        view.addSubview(pickerView)
        
        // set the picker to the middle of the long list
        pickerView.selectRow(pickerDataSize/2, inComponent: 0, animated: false)
    }
    
    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return String(row % 10)
    }
    
    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // do something with the resulting selected row
        
        // reset the picker to the middle of the long list
        let position = pickerDataSize/2 + row
        pickerView.selectRow(position, inComponent: 0, animated: false)
    }
    
    // columns count
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }
    
    // rows count
    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return pickerDataSize
    }
}

In this 2nd code snippet for our ViewController, we have created a large number, pickerDataSize, that tells our device how many rows there are. The neat thing is that we don’t need to create an array of 100K items. When the UIPickerViewDelegate’s titleForRow method is called, it uses the row to dynamically return a string for that given row. Progress!

As a bonus, I went ahead and added a call to pickerView.selectRow in the didSelectRow function. When the user finishes selecting a row, this call will silently (animated: false) move the selected row back to the middle of the rows. This will maintain the illusion of infinite scrolling. Success!

Here we can see the updated, dynamic picker view:

dynamic_ss