2012年9月26日水曜日

Unity の PostprocessBuildPlayer を使って Weak Framework を追加する方法

※2013/11/27追記: 第二版を公開しました。

UnityでiOSのアプリを作っていて困ることの一つに、iOSが提供するシステムフレームワークへのリンクをプロジェクトに追加するのが超面倒くさいという問題が挙げられます。UnityがiOSアプリを書きだした後、手動でXcode上からシステムフレームワークを追加してもいいのですが、これはとんでもなく面倒です。

そこでUnityジャパンの伊藤さんという方が PostprocessBuildPlayer という仕組みと Ruby の xcodeproj ライブラリを使ってビルド時に自動的にシステムフレームワークを追加する仕組みを公表してくださいました。お陰様で随分とはかどっていたのですが、その方法では実はシステムフレームワークをWeakリンク(Optionalリンク)することができなかったのです。これでは例えばSocial.frameworkを使うアプリをビルドしてiOS5で動かすと起動時に問答無用でクラッシュしてしまいます。困りました。iOS6/5両対応ができないと話になりません。

というわけで作りました。システムフレームワークをWeakリンクできるPostprocessBuildPlayerを。こちらです。
#!/usr/bin/env ruby
#
# PostprocessBuildPlayer
# Tested on Ruby 1.8.7, Gem 1.3.6, and xcodeproj 0.3.0
# Created by akisute (http://akisute.com)
# Licensed under The MIT License: http://opensource.org/licenses/mit-license.php
#
require 'rubygems'
require 'xcodeproj'
require 'pathname'
#
# Define utility functions
#
def add_system_frameworks_to_project(proj, framework_names, option=:required)
proj.targets.each do |target|
if target.name == "Unity-iPhone-simulator" then
next
end
framework_names.each { |framework_name|
framework = proj.add_system_framework(framework_name)
ref = Xcodeproj::Project::PBXBuildFile.new(proj, nil, { 'fileRef' => framework.uuid})
if option == :optional then
ref.settings = {"ATTRIBUTES" => ["Weak"] }
end
if target.respond_to?(:build_phases) then
# xcodeproj 0.3.0 or greater
phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) }
phase.build_files << ref
else
# xcodeproj 0.1.0
phase = target.buildPhases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) }
phase.files << ref
end
puts "Added system framework: " + framework_name + " as " + option.id2name
}
end
end
#
# Define build directory path
# -> Will be suppried as argv if run by Unity
# -> Else, assume UNITY_PROJECT_ROOT/build is a build directory
#
buildpath = (ARGV[0]) ? ARGV[0] : File.expand_path(File.dirname($0)) + "/../../build"
puts "PostprocessBuildPlayer running on build directory: " + buildpath
#
# Add System frameworks required to build
#
projpath = buildpath + "/Unity-iPhone.xcodeproj"
proj = Xcodeproj::Project.new(projpath)
add_system_frameworks_to_project(proj, ["StoreKit", "Security", "CoreText", "MessageUI"], :required)
add_system_frameworks_to_project(proj, ["Twitter", "Social"], :optional)
proj.save_as(projpath)


ライセンスはMITライセンスにしておきました。
使い方は大体見ればわかるかと思いますが、まず最初にgem経由でxcodeprojをインストールしておくこと。
sudo gem install xcodeproj
あとは上記のPostprocessBuildPlayerをUnityプロジェクトの /Assets/Editor 以下に配置して、コード中のフレームワーク名を指定している箇所をご自身のお好きなように変更してやればオッケーです。

Unity 3.5.5 以下で Game Center / iAd / UIImagePicker などを使用したアプリが iOS 6 でクラッシュする問題

現在 Unity 3.5.5 以下でビルドしたiOSアプリが、以下の条件をすべて満たしているとクラッシュしてしまう問題が発生しているようです。
  • UnityのiOSアプリビルド設定で、画面方向を横向き(Landscape Left/Landscape Right)のいずれかのみに設定している。
  • Pluginなどを経由してGame Centerを使用している。またはiAdやUIImagePickerなどのiOSが提供する特定のView Controllerを使用している。
  • iOS 6を搭載したiPhone/iPod Touch上で動作させる(iPadは大丈夫)。
詳細はこちら。
http://developer.coronalabs.com/forum/2012/09/17/ios-6-orientation-crash





すでにUnity側で問題は把握しているようで、現在修正版の3.5.6を用意してくださっているようなので、続報を待ちましょう。・・・といっても、すでにUnity 3.5.5以下でビルドされたiOSアプリをリリースしていて、しかもAsset Bundleを使用していたりすると、Asset Bundle間の後方互換性問題のためかなり厄介なことになると思われます。最悪過去のバージョンのサポートをすべて切る必要が出てくるかもしれません。


さて、ここからは技術話の余談です。

このクラッシュ問題なのですが、原因はiOS 6で変更になった画面回転(Auto Rotation)APIにあると思われます。iOS 6からはなんとこれまで画面回転に使用されていた
UIViewController shouldAutorotateToInterfaceOrientation:
が完全に廃止になっており、基本的には全く呼び出されないようになってしまっています。その代わりにより体型だった画面回転の仕組みが導入されています。iOS 6からの画面回転は、「アプリが対応する画面方向」と「各View Controllerが対応する画面方向」の2つによって画面の回転する方向が決定されるという仕組みになっています。

ここで、アプリが対応する画面方向は以下のように決定されます。
以下優先順位順に、
1. UIApplicationDelegate application:supportedInterfaceOrientationsForWindow: が返す向き。iOS 6以降のみ。
2. UIApplication supportedInterfaceOrientationsForWindow: が返す向き。iOS 6以降のみ。
3. Info.plist で指定されている UIInterfaceOrientation の向き。
各View Controllerが対応する画面方向は以下のように決定されます。
各ViewControllerが supportedInterfaceOrientations を実装しているなら、それが返す向き。
していないならば、
iPhoneだと UIInterfaceOrientationMaskAllButUpsideDown
iPadだと   UIInterfaceOrientationMaskAll
この2つの積集合をとって、両方が一致した向きに画面回転が行われる仕組みになっています。

では、ここで両者が全く一致しない場合はどうなるでしょう?答えは簡単でクラッシュします。ワオ。素敵。ふざけんなバカApple爆発しろ。

それを踏まえると、今回の問題でクラッシュしてしまう仕組みはこう考えられます。
  1. Unity 3.5.5以下が書き出すiOSアプリは当然ながらiOS 6に完全対応していない。
  2. Game Centerなど、Appleが提供しているUIコンポーネントは全てiOS 6の画面回転に対応しているが、それらのうち幾つかのものはiPhoneだと縦向き画面にしか対応していないものがある(iPadは基本縦横どちらでも表示できるように対応する必要があるとされているため、無事のようです)。
  3. Unityが書きだしたアプリは横向き画面にしか対応していないのに、上記のView Controllerは縦向きにしか対応していないと言い出すので、クラッシュする。
やれやれですね><
ちなみに対応策としては、アプリ自体は縦方向にも横方向にも対応しているというふうにapplication:supportedInterfaceOrientationsForWindow:メソッドを使って値を返すようにした上で、コンテンツを表示するUIViewをUIViewControllerに管理させるようにして、UIViewControllerのsupportedInterfaceOrientationsで横向き画面の値だけを返すようにするといいかんじになると思います。しかしながらiOS 5/6両方で上手く回る画面は結構大変そうです。