Hatena::ブログ(Diary)

A Day In The Life RSSフィード Twitter

2015-07-18

オフスクリーンレンダリングを使って画像を動的にアトラス化する

オフスクリーンレンダリングという手法があります。この手法をつかって動的に画像をアトラス化してスプライトフレームキャッシュ(SpriteFrameCache)に乗せることができると、必要最小限の画像をアトラス化して効率よくキャッシュできるのではないかということで試してみました。

まずはオフスクリーンレンダリングスプライトフレームキャッシュそれぞれの使い方を説明してから、その2つを組み合わせる方法を最後に紹介します。

オフスクリーンレンダリング

オフスクリーンレンダリングとは、テクスチャを動的に変更するためのテクニックです。動的にテクスチャを生成する時に画面の表示領域でやるのではなく画面の表示されないところで行います。オフスクリーンと言われるのはそのためです。

Cocos2d-x でオフスクリーンレンダリングを行うには RenderTexture クラスを使います。RenderTexture オブジェクトの beginWithClear メソッドと end メソッドの間に Sprite オブジェクトの描画処理(visit メソッド)を挟みます。

// オフスクリーンレンダリング用テクスチャ
Size winSize = Director::getInstance()->getWinSize();
RenderTexture *texture = RenderTexture::create(winSize.width, winSize.height);
// レンダリング開始
texture->beginWithClear(0, 0, 0, 0);

Size size = Director::getInstance()->getWinSize();
for (std::string fileName : files) {
  // スプライト生成
  auto sprite = Sprite::create("hoge.png");
  // 上下反転させる
  sprite->setScaleY(-1.0f);
  sprite->setPosition(Vec2(rand()%(int)size.width, rand()%(int)size.height));
  // 書き込み
  sprite->visit();
}

// レンダリング終了
texture->end();

RenderTexture オブジェクトと Sprite オブジェクトになんの関連もないのでこれでいいのか感はありますが、裏で Director オブジェクトがうまいことやってくれてます。あと RenderTexture には begin というメソッドもあるのですがたまにゴミが残って透過画像の描画でおかしくなる時があるので beginWithClear メソッドを使いましょう。Sprite オブジェクトに scale をかけて上下反転させているのは OpenGLテクスチャ座標が左上原点だからです。

スプライトフレームキャッシュ

画像を一つ一つ個別で描画するよりも、複数の画像をまとめた(アトラス化された)画像を使うことで描画効率が良くなります(ドローコールが減るなど)。Web でいうところの CSS スプライトに近い発想です。Cocos2d-x でアトラス化された画像を扱う場合は SpriteFrameCache クラスを使います。アトラス画像とその画像の中にどのような画像が配置されているかを管理するための plist ファイル(中身はXMLです)を開発者側であらかじめ用意してから SpriteFrameCache オブジェクトに plist ファイルを読み込ませます。そのようにすることでキャッシュからスプライトを生成することができます。プログラム的には以下のようになります。

// plistからキャッシュ生成
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("atlas.plist");
// スプライト生成
auto sprite = Sprite::createWithSpriteFrameName("hoge.png");

アトラス画像と plist ファイルの生成にはいろいろな方法があります。個人的には ShoeBox という無料のアトラス画像制作ツールをおすすめします。

複数の画像を選択して ShoeBox にドラッグするとアトラス化された画像と plist ファイルが生成されます。plist ファイルの中身は以下のようになっています(hoge.png と piyo.png 画像をまとめて atlas.png にまとめた例)。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>frames</key>
    <dict>
      <key>hoge.png</key>
      <dict>
        <key>frame</key>
        <string>{{0,0},{256,256}}</string>
        <key>offset</key>
        <string>{0,0}</string>
        <key>rotated</key>
        <false/>
        <key>sourceColorRect</key>
        <string>{{1,3},{447,173}}</string>
        <key>sourceSize</key>
        <string>{256,256}</string>
      </dict>
      <key>piyo.png</key>
      <dict>
        <key>frame</key>
        <string>{{0,0},{128,128}}</string>
        <key>offset</key>
        <string>{0,0}</string>
        <key>rotated</key>
        <false/>
        <key>sourceColorRect</key>
        <string>{{1,3},{447,173}}</string>
        <key>sourceSize</key>
        <string>{128,128}</string>
      </dict>
    </dict>
    <key>metadata</key>
    <dict>
      <key>format</key>
      <integer>2</integer>
      <key>size</key>
      <string>{512,512}</string>
      <key>textureFileName</key>
      <string>atlas.png</string>
    </dict>
  </dict>
</plist>

plist の形式や中のタグの意味なんかは以下のページを参考にしてください。

動的にアトラス画像を生成する

オフスクリーンレンダリングで生成した RenderTexture オブジェクトからテクスチャを取り出して、それをスプライトフレームキャッシュに追加すると画像を動的にアトラス化できます。

#include "cocos2d.h"

using namespace cocos2d

// ファイル(動的に変わる想定)
std::vector<std::string> files;
files.push_back("hoge.png");
files.push_back("piyo.png");
files.push_back("fuga.png");
files.push_back("foo.png");
files.push_back("bar.png");
files.push_back("buz.png");

// テクスチャの最大サイズ
Size winSize = Director::getInstance()->getWinSize();
int width = 0;
int height = 0;
int maxHeight = 0;

// オフスクリーンレンダリング用テクスチャ
RenderTexture *texture = RenderTexture::create(winSize.width, winSize.height);
texture->beginWithClear(0, 0, 0, 0);

// plist中の画像に関するデータを書き込むためのMap
ValueMap frames;

for (std::string fileName : files) {
  // スプライト生成
  auto sprite = Sprite::create(fileName);
  if (width + sprite->getContentSize().width > winSize.width) {
    height += maxHeight;
    width = 0;
    maxHeight = 0;
  }
  sprite->setAnchorPoint(Vec2(0, 0));
  sprite->setPosition(Vec2(width, height));
  // 上下反転するので
  sprite->setScaleY(-1.0f);
  // 書き込み
  sprite->visit();
  Director::getInstance()->getTextureCache()->removeTextureForKey(fileName);

  // plist情報の生成
  ValueMap frame;
  frame["frame"] = StringUtils::format("{{%f, %f}, {%f, %f}}",
                                       sprite->getPosition().x,
                                       sprite->getPosition().y,
                                       sprite->getContentSize().width,
                                       sprite->getContentSize().height);
  frame["offset"] = StringUtils::format("{%d, %d}", 0, 0);
  
  frame["rotated"] = false;
  frame["sourceColorRect"] = StringUtils::format("{{%f, %f}, {%f, %f}}", 0.0f, 0.0f,
                                                 sprite->getContentSize().width,
                                                 sprite->getContentSize().height);
  frame["sourceSize"] = StringUtils::format("{%f, %f}",
                                            sprite->getContentSize().width,
                                            sprite->getContentSize().height);
       
  width += sprite->getContentSize().width;
    
  if (maxHeight < sprite->getContentSize().height ) {
    maxHeight = sprite->getContentSize().height;
  }
  frames[fileName] = frame;
}

// レンダリング終了
texture->end();

// plistのメタ情報を生成
ValueMap plist;
plist["frames"] = frames;
ValueMap metadata;
metadata["format"] = 2; // いろいろ種類があるみたいだけど今回は2で
metadata["size"] = StringUtils::format("{%d, %d}", winSize.width, winSize.height);
metadata["textureFileName"] = "atlas.png";
plist["metadata"] = metadata;

std::string filePath = "atlas.plist";
// plist ファイルを保存
FileUtils::getInstance()->writeToFile(plist, filePath);
// キャッシュを生成
SpriteFrameCache::getInstance()->removeSpriteFramesFromFile(filePath);
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(filePath,
                               texture->getSprite()->getTexture());

// スプライとを使うときはこんな感じ
auto sprite = Sprite::createWithSpriteFrameName("hoge.png");

アトラス画像用の plist ファイルを生成する部分が複雑ですがそれ以外は特に問題ないかと思います。

すべての画像をアトラス化するには画像が多すぎて現実的ではないとか、サーバからの情報で使用する画像が動的に変わるとか、そんな時に使うと有効だと思います。

問題点が...。

今回紹介したオフスクリーンレンダリングの方法では、端末の画面サイズ以上の画像を生成することができません。次の記事でこのあたりを改良してみます。

// こっちじゃなくて
Size winSize = Director::getInstance()->getWinSize();
// こんな感じで画面サイズ以上の領域でも使いたい
GLint glSize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, & glSize);
const int TEXTURE_SIZE = a_val;

参考

2015-06-08

【Swift1.2対応】プロの力が身につく iPhone/iPadアプリケーション開発の教科書 Swift対応版

iOS が8.3にバージョンアップしたのに伴い Xcode が6.3に Swift が1.2にバージョンアップされました。Swift 1.2ではシンタックスエラーとなりビルドできなくなるコードがあります。本書で記載しているコードだけでなく、サンプルの自動生成コードにも変更が必要な箇所がありましたので修正しました。最新のサンプルコードはすべて Swift 1.2に対応済みです。

以降は、本書に記載されているプログラムの修正箇所になります。

Chapter2-1

p112 プログラム 4行目

訂正前

let button = UIButton.buttonWithType(UIButtonType.System)
    as UIButton

訂正後

let button = UIButton.buttonWithType(UIButtonType.System)
    as! UIButton
p112 プログラム 12行目

訂正前

fun respondToButtonClick(sender: UIButton!) {

訂正後

func respondToButtonClick(sender: UIButton) {
p115 1つ目のプログラム3行目

訂正前

func respondToButtonClick(sender: UIButton!) {

訂正後

func respondToButtonClick(sender: UIButton) {
p115 2つ目のプログラム5行目

訂正前

let button = UIButton.buttonWithType(UIButtonType.System)
    as UIButton

訂正後

let button = UIButton.buttonWithType(UIButtonType.System)
    as! UIButton
p115 2つ目のプログラム最後から1行目

訂正前

func respondToButtonClick(sender: UIButton!) {

訂正後

func respondToButtonClick(sender: UIButton) {

Chapter2-2

p127 2つ目のプログラム4行目

訂正前

func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {

訂正後

func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
p138 1つ目のプログラム2行目

訂正前

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {

訂正後

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
p138 1つ目のプログラム4行目

訂正前

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {

訂正後

override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
p138 1つ目のプログラム6行目

訂正前

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {

訂正後

override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
p138 1つ目のプログラム8行目

訂正前

override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {

訂正後

override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {

Chapter2-3

p148 2つ目のプログラム4行目

訂正前

let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
    forIndexPath:indexPath) as UITableViewCell

訂正後

let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
    forIndexPath:indexPath) as! UITableViewCell
p159 1つ目のプログラム2行目

訂正前

as UICollectionViewCell

訂正後

as! UICollectionViewCell
p159 2つ目のプログラム12行目

訂正前

let header = collectionView.dequeueReusableSupplementaryViewOfKind(
    kind,
    withReuseIdentifier: "Header",
    forIndexPath: indexPath) as UICollectionReusableView

訂正後

let header = collectionView.dequeueReusableSupplementaryViewOfKind(
    kind,
    withReuseIdentifier: "Header",
    forIndexPath: indexPath) as! UICollectionReusableView

Chapter3-2

p178 1つ目のプログラム5行目

訂正前

let button = UIButton.buttonWithType(UIButtonType.System)
    as UIButton

訂正後

let button = UIButton.buttonWithType(UIButtonType.System)
    as! UIButton

Chapter3-3

p191 2つ目のプログラム5行目

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as! Array<String>
p192 1つ目のプログラム2行目

前のページからの続きになっている箇所です。

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as! Array<String>
p196 1つ目のプログラム2行目

訂正前

self.name = aDecoder.decodeObjectForKey("name") as String 
self.address = aDecoder.decodeObjectForKey("address") as Address

訂正後

self.name = aDecoder.decodeObjectForKey("name") as! String 
self.address = aDecoder.decodeObjectForKey("address") as! Address
p196 2つ目のプログラム19行目

訂正前

self.zipCode = aDecoder.decodeObjectForKey("zipCode") as String
self.state = aDecoder.decodeObjectForKey("state") as String
self.city = aDecoder.decodeObjectForKey("city") as String
self.other = aDecoder.decodeObjectForKey("other") as String

訂正後

self.zipCode = aDecoder.decodeObjectForKey("zipCode") as! String
self.state = aDecoder.decodeObjectForKey("state") as! String
self.city = aDecoder.decodeObjectForKey("city") as! String
self.other = aDecoder.decodeObjectForKey("other") as! String
p197 1つ目のプログラム20行目
p198 1つ目のプログラム9行目

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as [String]

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as! [String]
p198 1つ目のプログラム12行目

訂正前

let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as Array<Person>

訂正後

let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as! Array<Person>
p202 1つ目のプログラム9行目
p203 1つ目のプログラム9行目
p205 1つ目のプログラム9行目
p206 2つ目のプログラム9行目

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as [String]

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as! [String]
p206 2つ目のプログラム15行目

訂正前

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person

訂正後

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as! NSData) as! Person
p219 1つ目のプログラム1行目

訂正前

let addressList = defaults.arrayForKey("address-list") as [NSData]

訂正後

let addressList = defaults.arrayForKey("address-list") as! [NSData]
p219 1つ目のプログラム3行目

訂正前

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person

訂正後

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as! Person
p229 1つ目のプログラム11行目

訂正前

let applicationDocumentsDirectory: NSURL = urls[urls.count-1] as NSURL

訂正後

let applicationDocumentsDirectory: NSURL = urls[urls.count-1] as! NSURL
p230 1つ目のプログラム2行目

訂正前

let model = NSEntityDescription.insertNewObjectForEntityForName("Event", inManagedObjectContext: context) as NSManagedObject

訂正後

let model = NSEntityDescription.insertNewObjectForEntityForName("Event", inManagedObjectContext: context) as! NSManagedObject
p252 2つ目のプログラム3行目

訂正前

let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject

訂正後

let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
p254 2つ目のプログラム7行目

訂正前

let person = NSEntityDescription.insertNewObjectForEntityForName(entity.name!, inManagedObjectContext: context) as NSManagedObject

訂正後

let person = NSEntityDescription.insertNewObjectForEntityForName(entity.name!, inManagedObjectContext: context) as! NSManagedObject
p257 1つ目のプログラム5行目

訂正前

let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

訂正後

let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
p277 1つ目のプログラム16行目

訂正前

let dict = NSMutableDictionary()

訂正後

var dict = [String: AnyObject]()

Chapter4-2

p312 1つ目のプログラム3行目から4行目

訂正前

let ope = object as HttpOperation
let responseString: String = NSString(data: ope.responseData, encoding:NSUTF8StringEncoding)!

訂正後

let ope = object as! HttpOperation
let responseString: String = NSString(data: ope.responseData, encoding:NSUTF8StringEncoding) as String!

Chapter4-3

p318 2つ目のプログラム9行目

訂正前

let payment = SKPayment(product: product as SKProduct)

訂正後

let payment = SKPayment(product: product as! SKProduct)
p319 1つ目のプログラム3行目
p323 1つ目のプログラム3行目
p327 1つ目のプログラム22行目

訂正前

for transaction in transactions as [SKPaymentTransaction] {

訂正後

for transaction in transactions as! [SKPaymentTransaction] {
p333 1つ目のプログラム8行目

訂正前

let productRequest = SKProductsRequest(productIdentifiers: set)

訂正後

let productRequest = SKProductsRequest(productIdentifiers: set as Set<NSObject>)
p334 1つ目のプログラム1行目

訂正前

let payment = SKPayment(product: product as SKProduct)

訂正後

let payment = SKPayment(product: product as! SKProduct)
p334 1つ目のプログラム7行目、29行目、44行目

訂正前

for transaction in transactions as [SKPaymentTransaction] {

訂正後

for transaction in transactions as! [SKPaymentTransaction] {

関連記事

参考

2015-03-18

Cocos2d-x でネイティブ連携する方法

Cocos2d-x でゲームを開発していると広告を表示させたり、ランキングを追加したり、課金を入れたり、SNSTwitter と連携したり、アクセス解析をしたり、など iOSAndroid の機能を使わないと実現できない機能があります(このようにプラットフォームと Cocos2d-x を連携させることをネイティブ連携と呼びます)。Cocos2d-x には Plugin-X というネイティブ連携のための仕組みがあるのですが、提供されている機能が少なくまた導入が複雑(特に Android)で導入するメリットがあまりないのが実情です。

どうせなら自前で実装したほうがいろいろと便利だし iOSAndroid の知識も深まるのでこの記事では Cocos2d-x 3.4 Final でネイティブ連携する方法について説明します。ネイティブ連携の仕組みは一度開発してしまえば似たようなコードを書かずに、Objective-CJava を書くだけで機能追加できるようになるので覚えておいて損はないと思います。

ネイティブ連携の仕組み

ネイティブ連携は新規にクラスを作成してヘッダファイルのみ共通にして実装ファイルを iOS 用、Android 用と分けます。iOS の場合は Objective-C++ で実装を書きます。Android の場合は実装部分に JNI の呼び出しコードを書きます。実際の処理はあらかじめ Java 側に用意しておきます。

ネイティブ連携用クラスの作成

それでは連携用の NativeLauncher クラスを作成します。以下のようにヘッダと実装ファイルを作成してください。

  • NativeLauncher.h
  • NativeLauncher.mm(iOS用)
  • NativeLauncher.cpp(Android用)

例えばネイティブ連携をしてハイスコアを登録する機能を作成する場合は以下のようにクラスにメソッドを定義します。

#include "cocos2d.h"

class NativeLauncher {
public:
  // ハイスコアを登録する
  static void postHighScore(int score);
};

Xcode の設定

開発に Xcodeを使っている場合は NativeLauncher.cpp ファイルをコンパイル対象から外す必要があります。

削除手順は Project Navigator からプロジェクトのルートを選択してプロジェクトの詳細画面を表示します。次に「TARGETS」の「プロジェクト名 iOS」を選択して「Compile Sources」から NativeLauncher.cpp を削除してください。

Xcodeの画像

NativeLauncher.mm の実装

iOS の場合は Objective-C++ が使えますのでメソッド中に直接 Objective-C のコードを書いていきます。

#include "NativeLauncher.h"

#import <GameKit/GameKit.h>

void NativeLauncher::postHighScore(int score)
{
  GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
  if([localPlayer isAuthenticated]) {
    // 処理...
  }
}

Java の実装と NativeLauncher.cpp の実装

Android の場合は少し複雑でまず Java で実装を書いてからそのコードを C++ から JNI を経由して呼び出すことになります。Androidネイティブ連携コードは基本的には proj.android にある AppActivity クラスに書きます。

AppActivity クラスの詳細はこちらの記事を参照してください。

package org.cocos2dx.cpp;

import org.cocos2dx.lib.Cocos2dxActivity;

public class AppActivity extends Cocos2dxActivity {
  // 気持ち悪いけど static メソッドから参照するときに必要
  private static AppActivity me = null;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    me = this;
  }
  // JNI から呼び出されるメソッド
  public static void postHighScore(int score) {
    // UIスレッドで実行
    me.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        // 処理...
      }			
    });
  }
}

あとは C++ からJNI で postHighScore メソッドを呼び出します。

#include "NativeLauncher.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>

#define CLASS_NAME "org/cocos2dx/cpp/AppActivity"

void NativeLauncher::postHighScore(int score)
{
  JniMethodInfo methodInfo;
  if (!JniHelper::getStaticMethodInfo(methodInfo, CLASS_NAME, "postHighScore", "(I)V")) {
    return;
  }    
  methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID, score);
  methodInfo.env->DeleteLocalRef(methodInfo.classID);
}

Cocos2d-x が JniHelper という便利なクラスを用意してくれているのでそれを使います。JniHelper クラスの getStaticMethodInfo メソッドの第4引数暗号じみてよくわからないかもしれません。int 型の引数を取り戻り値は void という意味です。

参考

関連記事

2015-03-13

【正誤表】プロの力が身につく iPhone/iPadアプリケーション開発の教科書 Swift対応版

「プロの力が身につく iPhone/iPadアプリケーション開発の教科書」をお買い上げの皆様、ありがとうございます。

読者様からご指摘を頂き、誤りがありましたので訂正させていただきます。

2-3テーブルビューとコレクションビュー

148ページ、プログラム2つ目の下から3行目

訂正前

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

訂正後

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel?.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

加えてサンプルプログラムにも修正が必要でしたので修正しました。GitHub に push 済みです。

CollectionViewサンプルプログラム

ViewController クラス viewDidLoad メソッド、本の内容に訂正はありません。GitHub に push 済みです。

修正前

override func viewDidLoad() {
  super.viewDidLoad()      
  self.collectionView.contentInset = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
}

修正後

override func viewDidLoad() {
  super.viewDidLoad()      
  self.collectionView?.contentInset = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
}
Navigationサンプルプログラム

ViewController クラス tableView:cellForRowAtIndexPath: メソッド、本の内容に訂正はありません。GitHub に push 済みです。

修正前

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

修正後

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel?.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

3-3データをフラッシュドライブに保存する

191ページ、プログラム2つ目の7行目(4/3追加)

訂正前

@IBAction func respondToArchiveButton() {
  // ファイルパスの取得
  let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as [String]
  // 保存するファイルの名前
  let filePath = String(paths[0]) + "data.dat"
  // 保存するデータ、氏名と住所
  let array = ["山田太郎", "104-0061", "東京都", "中央区", "銀座1丁目"]
  // アーカイブしてdata.datというファイル名で保存する
  let successful = NSKeyedArchiver.archiveRootObject(array, toFile: filePath)
  if successful {
    println("データの保存に成功しました。")
  }
}

訂正後

@IBAction func respondToArchiveButton() {
  // ファイルパスの取得
  let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as [String]
  // 保存するファイルの名前
  let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
  // 保存するデータ、氏名と住所
  let array = ["山田太郎", "104-0061", "東京都", "中央区", "銀座1丁目"]
  // アーカイブしてdata.datというファイル名で保存する
  let successful = NSKeyedArchiver.archiveRootObject(array, toFile: filePath)
  if successful {
    println("データの保存に成功しました。")
  }
}
192ページ、プログラム1つ目の3行目(4/3追加)

訂正前

    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>
  let filePath = String(paths[0]) + "data.dat"
  if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Array<String> {
    for str in array {
      println(str)
    }
  } else {
    println("データがありません")
  }
}

訂正後

    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>
  let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
  if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Array<String> {
    for str in array {
      println(str)
    }
  } else {
    println("データがありません")
  }
}
197ページ、プログラム1つ目の下から8行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToArchiveButton() {
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    // 保存するデータを配列にまとめる
    let people = [taroYamada, hanakoYamada, jiroTanaka]
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.dat"
    // アーカイブしてファイルに保存
    let successful = NSKeyedArchiver.archiveRootObject(people, toFile: filePath)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToArchiveButton() {
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    // 保存するデータを配列にまとめる
    let people = [taroYamada, hanakoYamada, jiroTanaka]
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // アーカイブしてファイルに保存
    let successful = NSKeyedArchiver.archiveRootObject(people, toFile: filePath)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}
198ページ、プログラム1つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToUnarchiveButton() {
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.dat"
    // アンアーカイブする
    let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as Array<Person>
    for person in array {
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToUnarchiveButton() {
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // アンアーカイブする
    let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as Array<Person>
    for person in array {
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}
202ページ、プログラム1つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // 都道府県データ(NSArray型にキャストする)
    let array = ["北海道", "青森県", "岩手県", "秋田県", "宮城県", "山形県"] as NSArray
    let successful = array.writeToFile(filePath, atomically: false)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // 都道府県データ(NSArray型にキャストする)
    let array = ["北海道",
                      "青森県",
                      "岩手県", 
                      "秋田県",
                      "宮城県",
                      "山形県"] as NSArray
    let successful = array.writeToFile(filePath, atomically: false)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}
203ページ、プログラム1つ目の下から8行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      println(data)
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      println(data)
    }
  }
}
205ページ、プログラム1つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    let archivedTaroYamada = NSKeyedArchiver.archivedDataWithRootObject(taroYamada)
    let archivedHanakoYamada = NSKeyedArchiver.archivedDataWithRootObject(hanakoYamada)

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    let archivedTaroYamada = NSKeyedArchiver.archivedDataWithRootObject(taroYamada)
    let archivedHanakoYamada = NSKeyedArchiver.archivedDataWithRootObject(hanakoYamada)
206ページ、プログラム2つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      // 読み込んだオブジェクトをアンアーカイブする
      let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      // 読み込んだオブジェクトをアンアーカイブする
      let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}
補足

191ページから206ページにかけての訂正はすべてファイルパスの文字列連結をしている以下のプログラムが実機で実行時エラーになるのが原因です。

// 実行時エラー
let filePath = String(paths[0]) + "data.plist"

以下のようにファイル名の前にスラッシュを付けるだけで修正できますが、

let filePath = String(paths[0]) + "/data.dat"

URL の操作には以下のように stringByAppendingPathComponent メソッドを使ったほうがより安全です。

let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")

Swift 1.2の変更への対応(2015/5/25追記)

iOS が8.3にバージョンアップしたのに伴い Xcode が6.3に Swift が1.2にバージョンアップされました。Swift 1.2ではシンタックスエラーとなりビルドできなくなるコードがあります。本書で記載しているコードだけでなく、サンプルの自動生成コードにも変更が必要な箇所が発生しました。2015年5月25日にサンプルプログラムコンパイルエラーを修正したバージョンをGitHubにアップしました。iOS8.3で開発されている読者様はGitHubのサンプルコードページより最新版をダウンロードしてください。また Swift 1.2への対応箇所を以下の記事にまとめましたのでそちらも参考にしてください。

今後、間違いを見つけ次第この記事に追記していきます。

間違いを指摘してくださいました読者様ありがとうございました。今後も本書の間違いや訂正があればこの記事に追記して行きます。訂正があったことをこの場をお借りしてお詫びします。引き続き本書をよろしくお願いします。

関連記事

参考

2015-03-02

Androidでui::ScrollViewの背景が緑色になる時の対処方法

Cocos2d-x(3.4 Final) の ui::ScrollView クラスを使うとAndroidで背景色が緑色になる現象が発生しました(iOS ではこの現象は発生しません)。

こちらのページに載っている方法で解決することができました。

具体的な解消法は proj.android/src/org/cocos2dx/cpp/AppActivity.java に以下の行を追加するだけです。

public class AppActivity extends Cocos2dxActivity {
  :
  :  省略
  :
  @Override
  public Cocos2dxGLSurfaceView onCreateView() {
    Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
    // 追加する
    glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
    return glSurfaceView;
  }
}

OpenGLの設定がどうやらだめだったぽいです。

参考