Developing an Apple TV app is different

A few days ago I had the idea to develop an Apple TV app for the payleven InApp payment product. The app shows how the payleven InApp product helps developers accepting card payments on the new tvOS platform. An InApp product overview can be found at the GitHub payleven repository InApp-SDK-iOS. As usual the initial time estimation was short, knowing that we have the framework in place and the tvOS app should only have a few views. The initial assumption was wrong. I spent the most time reading and understanding the new concepts.

Development for Apple TV is a little bit different than for iOS. The most important difference is that the user interacts with the application via a remote control and can not touch the UI directly. Therefore Apple introduced the focus engine which is responsible to focus elements on screen. Another point to consider when designing the UX for an AppleTV application is the fact that the user will sit three to five meters away from the screen and often with other people.

All those aspects are important to make reasonable design decisions.

During development I faced the following challenges:

  • How can I set the focus on an element which is not horizontally or vertically in the same column or row
  • Is it possible to get the default focus animation for a custom UIButton
  • How can I compile a already existing framework for tvOS without having the framework twice in the workspace
  • Why is a UISplitViewController not dismissing as expected if using a unwind Segue

Manually setting the focus

The screen above shows a focused UICollectionViewCell which is vertically not above the order button. That’s why it was impossible to navigate to the order button without any further adjustments. I figured out creating a UISwipeGestureRecognizer which listens to swipe down is the key.

If a swipe down is detected the preferredFocusedView: method returns the proceed button as preferred focused view.

In the beginning this sounds strange, but the focus engine and the UIKit checks only if there is a focusable element horizontally or vertically.

For more details please check the tvOS documentation here “Deciding Where to Move Focus”.

Custom UIButton which has the default focus animation

Using a default UIButton works of course totally fine. All the known feedback and features are working as expected. With tvOS you get as well the focus animation which enlarges the element. If you change the UIButton type to UIButtonTypeCustom the tvOS element focus animation is not available anymore. To achieve a similar effect subclass UIButton and override the method “didUpdateFocusInContext:withAnimationCoordinator”.

If the “didUpdateFocusInContext:withAnimationCoordinator” method of the UIButton is called we use CGAffineTransformMakeScale(1.1, 1.1) to transform the element and manipulate the layer of the view to get a shadow. If the nextFocusedView is not our button anymore we reset the transformation with CGAffineTransformIdentity.

For the mentioned app the “Order” button should have two background images to make the state more visible.

Normal:

Focused:

Create two images for the button background with your favourite design tool and set the images according the “button state config” (Default/Focused/…) inside the Interface Builder.

Use unwind segue with only one animation

There is one thing I couldn’t sort out so far. If I use a UISplitViewController somewhere in the middle of the app scene flow, it is not possible to dismiss/unwind to the initial view controller without showing the UISplitViewController half a second. Even hiding the master view is not solving the issue.

That’s why I removed the UISplitViewController and did the same UI with a normal UIViewController + UITableView. If anyone knows the reason for that behaviour please send me a direct message via twitter (@kopf_markus) or mail. I couldn’t find any hint inside the UISplitViewController documentation according such a behaviour. The UISplitViewController is also not prohibited for tvOS.

Compile a framework for tvOS

Since we like to use our own code for more than one project we build as often as possible a static library or a framework for external use.

For creating the InApp framework we have to do the following steps.

1. Creating the framework structure

set -e
export FRAMEWORK_LOCN=”${BUILT_PRODUCTS_DIR}/PaylevenInAppSDK.framework”
# Create the path to the real Headers die
mkdir -p “${FRAMEWORK_LOCN}/Versions/A/Headers”
# Create the required symlinks
/bin/ln -sfh A “${FRAMEWORK_LOCN}/Versions/Current”
/bin/ln -sfh Versions/Current/Headers “${FRAMEWORK_LOCN}/Headers”
/bin/ln -sfh “Versions/Current/PaylevenInAppSDK” \
“${FRAMEWORK_LOCN}/PaylevenInAppSDK”
# Copy the public headers into the framework
/bin/cp -a “${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/” \
“${FRAMEWORK_LOCN}/Versions/A/Headers”

2. Build the project with all platforms you’re going to support
set -e
# If we’re already inside this script then die
if [ -n “$PLV_MULTIPLATFORM_BUILD_IN_PROGRESS” ]; then
echo “DIE WITH MULTIPLATFORM BUILD ALREADY IN PROGRESS”
exit 0
fi

export PLV_MULTIPLATFORM_BUILD_IN_PROGRESS=1

PLV_FRAMEWORK_NAME=”PaylevenInAppSDK”
PLV_INPUT_STATIC_LIB=”libPaylevenInAppSDKtvOS.a”
PLV_FRAMEWORK_LOCATION=”${BUILT_PRODUCTS_DIR}/${PLV_FRAMEWORK_NAME}.framework”

function make_fat_library {
# Will smash 2 static libs together
# make_fat_library in1 in2 out
xcrun lipo -detailed_info “${1}”
xcrun lipo -detailed_info “${2}”
xcrun lipo -create “${1}” “${2}” -output “${3}”
}

#appletvos
# 1 — Extract the platform (appletvos) from the SDK name
if [[ “$SDK_NAME” =~ ([A-Za-z]+) ]]; then
RW_SDK_PLATFORM=${BASH_REMATCH[1]}
else
echo “Could not find platform name from SDK_NAME: $SDK_NAME”
exit 1
fi

# 2 — Extract the version from the SDK
if [[ “$SDK_NAME” =~ ([0–9]+.*$) ]]; then
RW_SDK_VERSION=${BASH_REMATCH[1]}
else
echo “Could not find sdk version from SDK_NAME: $SDK_NAME”
exit 1
fi

# 3 — Determine the other platform
if [ “$RW_SDK_PLATFORM” == “appletvos” ]; then
RW_OTHER_PLATFORM=appletvsimulator
else
RW_OTHER_PLATFORM=appletvos
fi

# 4 — Find the build directory
if [[ “$BUILT_PRODUCTS_DIR” =~ (.*)$RW_SDK_PLATFORM$ ]]; then
RW_OTHER_BUILT_PRODUCTS_DIR=”${BASH_REMATCH[1]}${RW_OTHER_PLATFORM}”
else
exit 1
fi

# Build for appletvos
xcrun xcodebuild -project “${PROJECT_FILE_PATH}” \
-target “${TARGET_NAME}” \
-configuration “${CONFIGURATION}” \
-sdk “appletvos” \
ONLY_ACTIVE_ARCH=NO \
BUILD_DIR=”${BUILD_DIR}” \
OBJROOT=”${OBJROOT}” \
BUILD_ROOT=”${BUILD_ROOT}” \
SYMROOT=”${SYMROOT}”

# Build for appletvsimulator
xcrun xcodebuild -project “${PROJECT_FILE_PATH}” \
-target “${TARGET_NAME}” \
-configuration “${CONFIGURATION}” \
-sdk “appletvsimulator” \
ONLY_ACTIVE_ARCH=NO \
BUILD_DIR=”${BUILD_DIR}” \
OBJROOT=”${OBJROOT}” \
BUILD_ROOT=”${BUILD_ROOT}” \
SYMROOT=”${SYMROOT}”

# Join the 2 static libs into 1 and push into the .framework
make_fat_library “${BUILT_PRODUCTS_DIR}/${PLV_INPUT_STATIC_LIB}” \
“${RW_OTHER_BUILT_PRODUCTS_DIR}/${PLV_INPUT_STATIC_LIB}” \
“${PLV_FRAMEWORK_LOCATION}/Versions/A/${PLV_FRAMEWORK_NAME}”

# Ensure that the framework is present in both platform’s build directories
cp -a “${PLV_FRAMEWORK_LOCATION}/Versions/A/${PLV_FRAMEWORK_NAME}” \
“${RW_OTHER_BUILT_PRODUCTS_DIR}/${PLV_FRAMEWORK_NAME}.framework/Versions/A/${PLV_FRAMEWORK_NAME}”

# Copy the framework to the example project
ditto -V “${PLV_FRAMEWORK_LOCATION}” “../PaylevenInAPPTvOsSample/${PLV_FRAMEWORK_NAME}.framework”

export PLV_MULTIPLATFORM_BUILD_IN_PROGRESS=0

The framework builds automatically if you use this two scripts in a Xcode target. Since we build for tvOS it’s important that the Supported Platform in the Architectures category is set to tvOS.

Unfortunately it isn’t possible to build one fat library which contains iOS and tvOS architecture at the same time.

Conclusion

tvOS is a nice addition to iOS and the learning curve is not steep. One of the main key points is to understand the focus engine, after reading the documentation and playing around with some basic samples the focus engine becomes more and more logical.

Designing and creating the UX for Apple TV is a different story, common concepts which are working for iOS devices are not working for Apple TV at all. This is the same story for the AppleWatch. It’s crucial to understand how user interacts with apps if they are sitting at home on their couch.

The source code for the mentioned project is available here.