iOS既存プロジェクトにテストを追加する場合のハマりどころ

一番良いのは常に⌘Uを押してテストが通るか確認すること。
ある程度開発が進んでから、テストを書こうとするとどこに原因があるのか探すのに時間がかかる

Objective-Cのコードを使っている場合

テストTargetsに「Objective-C Bridging Header」の指定が漏れている


CocoaPodsを使ってる

ProjectのConfigurationsにPods.debugの指定が漏れている


CocoaPodsのresourceファイルがある

Build Phasesに「Copy Pods Resources」が指定されていない


上記の対応をするとうまくテストを実行する事ができました。
今回のプロジェクトはJSONのレスポンスをそのままCoreDataに保存するライブラリを使いました。
https://github.com/hrk-ys/HRKModelTransfer

iOS UIImage+ImageEffectsの調整をするサンプルコード

Blurを使ってぼかした画像を表示したい場合、iOS7もサポートしていると、iOS8から導入されたUIBlurEffectを使う事ができません。

ほかにもすでにCococaPodsなどでたくさん便利なものが出回っているので、実装自体そんなに大変ではないと思います。

が、デザイナーさんが表現したいぼかし具合を作るのがなかなか難しかったので、実際にパラメータをいじりながら調整できるサンプルコードを書きました。

今回対象にしているのは、Appleのサンプルコードです。

https://developer.apple.com/library/ios/samplecode/UIImageEffects/Introduction/Intro.html#//apple_ref/doc/uid/DTS40013396-Intro-DontLinkElementID_2


実際につくったものはここ

https://github.com/hrk-ys/BlurSample


実装 

UIImageEffects.h
+ (UIImage*)imageByApplyingBlurToImage:(UIImage*)inputImage
                            withRadius:(CGFloat)blurRadius
                             tintColor:(UIColor *)tintColor
                 saturationDeltaFactor:(CGFloat)saturationDeltaFactor
                             maskImage:(UIImage *)maskImage;

blurRadius、tintColor、saturationDeltaFactorをそれぞれSliderやColor PickerっぽいUIで設定できます





react-nativeのnpmモジュールを作成してみる

Static Libraryの作成

XcodeでNew>Projectをする

iOS、Framework & Libraryを選択し、Cocoa Touch Static Libraryを選択

Header Search Path

$(SRCROOT)/../../React
$(SRCROOT)/../react-native/React
$(SRCROOT)/node_modules/react-native/React

BridgeModuleの作成

githubを参照

https://github.com/hrk-ys/react-native-userdefaults/tree/master/RNUserDefaultsManager

JS

package.json

$ npm init

package名やversionなど適度に変更

{
  "name": "react-native-userdefaults",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT"
}

index.js

github参照

https://github.com/hrk-ys/react-native-userdefaults/blob/master/index.js

Example

$ react-native init UserDefaultsExample

package.json

依存モジュールに作成中のモジュールを追加

"dependencies": {
  "react-native": "^0.4.4",
  "react-native-userdefaults": "file:../"
}

Nativeプロジェクト追加

Library に node_modules/react-native-userdefaults/RNUserDefaults.xcodeproj/ を追加

Linked Frameworks and LibrariesにlibRNUserDefaults.aを追加

開発スタイル

これが正しいのか不明・・・

index.jsやRNUserDefaultsManager.[mh]を更新したら、Exampleプロジェクトの方で、以下を実行

$npm uninstall react-native-userdefaults
$npm install

ReactNativeでImages.xcassetsの画像を使う方法

前回書いた通り、


pod 'React/RCTImage'

使うところは



ただし、そのままではファイルが読み込めないので、
起動スクリプトにImage.xcassetsのディレクトリの場所を指定する


(JS_DIR=`pwd`/ReactComponent; ASSET_DIR=`pwd`//Images.xcassets; cd Pods/React; npm run start -- --root $JS_DIR --assetRoots $ASSET_DIR)


まだまだはまりどころが多いな・・・。

ReactNativeを使ってみたメモ Tips

jsからnativeを呼び出す

参考 http://facebook.github.io/react-native/docs/nativemodulesios.html#content

Obje-C

#import "RCTBridgeModule.h"

@interface SampleManager : NSObject <RCTBridgeModule>
@end

@implementation SampleManager

RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(callFunc:(NSString *)name param:(NSString *)param dict:(NSDictionary*)dict findEvents:(RCTResponseSenderBlock)callback)
{
    NSLog(@"name: %@", name);
    NSLog(@"str:  %@", param);
    NSLog(@"dict: %@", dict);


    callback(@[ [NSNull null], @{ @"hoge": @"val" } ]);
}

@end

JS

var SampleManager = require('NativeModules').SampleManager;
SampleManager.callFunc(
  'action',
  'string_param1',
  { foo: 'bar'},
  (error, ret) => {
    if (error) {
      console.error(error);
    } else {
      console.log(ret);
    }
  }
);

nativeからjaコードを呼び出す

RCTRootViewやBridgeModuleのインスタンスにbridgeがあるので、それを使う

ここは公式ドキュメントもちょっと間違ってました

Obje-C

#import "RCTBridge.h"
#import "RCTEventDispatcher.h"

[self.rootView.bridge.eventDispatcher sendDeviceEventWithName:@"callFuncName"
                                             body:@{@"name": @"foo"}];

JS


var subscription;
ar SimpleApp = React.createClass({
  callFromNative: function(params) {
    console.log(params);
    this.setState({ name: params.name });
  },

  componentDidMount: function() {
    // 登録
    subscription = DeviceEventEmitter.addListener('callFuncName', this.callFromNative);
  },
  componentWillUnmount: function() {
    // 解除
    subscription.remove();
  },

  ...
});

Nativeで定義したViewを使う

Swift未対応

Obje-C

  • RCTViewManagerを継承する
  • RCT_EXPORT_MODULE()
  • viewメソッドでViewを返す
#import "RCTViewManager.h"
@interface RCTSampleViewManager : RCTViewManager
@end



@implementation RCTSampleViewManager

RCT_EXPORT_MODULE()

- (UIView *)view
{
    UIView* view = [[UIView alloc] init];
    view.frame = CGRectMake(0, 0, 100, 100);
    view.backgroundColor = [UIColor greenColor];
    UILabel* l = [[UILabel alloc] init];
    l.text = @"hogehoge";
    l.textColor = [UIColor redColor];

    [l sizeToFit];
    [view addSubview:l];
    return view;
}


@end

JS

SampleView.js

'use strict';

var { requireNativeComponent } = require('react-native');
module.exports = requireNativeComponent('RCTSampleView', null);

index.ios.js

var SampleView = require('./SampleView');

...

render() {
  return (
    <View style={styles.container}>
      <Text>Hello ReactNative!!!</Text>
      <SampleView />
    </View>
  );
}

データの永続化

http://facebook.github.io/react-native/docs/asyncstorage.html#content

Cookie

ネイティブとで使っているCookieを引き継ぐことは可能?

無理やりくっつければ可能

#import "ReactNativeSupport.h"

@implementation ReactNativeSupport

RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(requestCookies: (RCTResponseSenderBlock)callback)
{

    NSDictionary* cookies = @{ @"session_id" : @"hogehogeho" };
    callback(@[ [NSNull null], cookies ]);
}

@end

JS

var cookie;

fetchData() {

  var cookie = "";
  for (var name in cookies) {
    cookie += name + "=" + cookies[name] + ";";
  }
  fetch(API_URL,
      { method: 'POST',
        body: JSON.stringify({"foo":"hoge"}),
        headers: {
          'cookie': cookie,
        }
      })
      .then((response) => {
        console.log(response.headers.map['set-cookie']); // Cookieが取得できる
        return response.json();
      })
      .then((responseData) => {
          console.log(responseData);
      })
      .catch((error) => {
          console.warn(error);
      });

}
componentDidMount() {
  var Support = require('NativeModules').ReactNativeSupport;
  Support.requestCookies(
    (error, ret) => {
      if (error) {
        console.error(error);
      } else {
        cookies = ret;
        this.fetchData();
      }
    }
  );

headersで指定しない場合は、responseにset-cookieが入ってきても設定されない。

一度headersで指定すれば、次のアクセスからは指定されている

resourceの画像を使う方法

ベクター画像だとうまく動かなかった

pod 'React/RCTImage'
<Image source={require('image!image_name')} />

ハマりどころ

package.jsonは必要!

cocoapodsで作ったプロジェクトや、Integration with Existing Appで作ったプロジェクトでは packega.jsonを作らないと、別ファイルの読み込みができない。

nodeやってる人には常識かな?

0.4.0以下だとNativeのCustomビューが使えない

コードを細部まで追ってないですが、0.4.1以降を使わないとNativeで定義したViewを使うことができない

ベクター画像は使えない

ドキュメントに書いてないけど読み込めない

ReactNativeを触ってみる


ReactNativeとは

  • Viewをコンポーネント単位で表示するためのライブラリ
  • Javascriptで書いて、ネイティブのViewでレンダリングされるため高速

実現したいこと

  • Appleの審査を待たずにアプリをバージョンアップさせたい
  • 一定のパフォーマンスは保ちたい
  • アプリ全体ではなく、一部分の置き換え

前提

  • 既存システムをReactNativeで置き換える
  • ほとんどネイティブの機能を使っていない

検討

  • 実はwebviewでもよいかも、パフォーマンスの比較もしたい

導入

  • http://www.reactnative.com/
  • http://facebook.github.io/react-native/docs/getting-started.html#content
    brew install node brew install watchman brew install flow
    npm install -g react-native-cli

サンプルプロジェクトの作成

react-native init AwesomeProject
AwesomeProjectディレクトリがつくられる - AwesomeProject.xcodeproj - iOS - node_modules - package.json

起動

Xcodeを立ち上げて、⌘+Rでいつも通り起動
サンプルプロジェクトでは、ビルド時にnodeを起動している

レンダリングするjsを指定

レンダリングするjsはアプリにインストールしたファイルからも、webからも取得することが可能
// webから取得する場合
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"];

// アプリ内のファイルを使う場合
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

// 描画させる
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                 moduleName:@"AwesomeProject"
                                                 launchOptions:launchOptions];
アプリ内にファイルを置く場合は以下のコマンドで取得
$ curl 'http://localhost:8081/index.ios.bundle?dev=false&minify=true' -o iOS/main.jsbundle

Cmd + Rで再読み込みできない場合

Simulator の Hardware > keyboard の設定を確認

既存プロジェクトへの導入

CocoaPods

pod 'React'
pod 'React/RCTText'
Bridge-Header.hの追加
#import <RCTRootView.h>

iOS App

ViewControllerのviewなどにコードから追加する
@IBOutlet weak var wrapView: UIView!
var rootView:RCTRootView? = nil

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    var jsCodeLocation = NSURL(string:"http://localhost:8081/index.ios.bundle")

    rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "SimpleApp", launchOptions: nil)
    rootView!.frame = wrapView.bounds

    wrapView.addSubview(rootView!)
}

React Native App作成

ReactComponentディレクトリを作り中身は index.ios.js を置く
'use strict';

var React = require('react-native');
var {
  Text,
  View
} = React;

var styles = React.StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'red'
  }
});

class SimpleApp extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>This is a simple application.</Text>
      </View>
    )
  }
}

React.AppRegistry.registerComponent('SimpleApp', () => SimpleApp);

開発サーバの起動

(JS_DIR=`pwd`/ReactComponent; cd Pods/React; npm run start -- --root $JS_DIR)

デバッグ

RCTWebSocketDebuggerを追加すると⌘Rで更新ができる
pod 'React/RCTWebSocketDebugger
⌘+Ctl+Zでデバッグメニューを表示させるには、シェイクジェスチャーのdelegateを呼ぶ必要がある
override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent) {
    rootView?.motionEnded(motion, withEvent: event)
}

Swift、UILabelで表示する文字列の高さを取得する

UILabelの高さ取得に気をつけること

1. xibやstoryboradの指定したFontと、プログラムで指定しているフォントは同じかどうか
2. viewDidLoad時ではまだviewのサイズが決まっていない

ロジック

NSString
func boundingRectWithSize(size: CGSize, options: NSStringDrawingOptions, attributes: [NSObject : AnyObject]!, context: NSStringDrawingContext!) -> CGRect

NSAttributedString
func boundingRectWithSize(size: CGSize, options: NSStringDrawingOptions, context: NSStringDrawingContext?) -> CGRect


どちらも使い方は同じ、必要な最大領域、オプションを指定すれば、描画に必要なサイズが取得可能

サンプル

NSString

    func heightWithText(text: String) -> CGFloat {
       
        let horizonMergin:CGFloat = 32
        let verticalMergin:CGFloat = 32
        
        let maxSize = CGSize(width: CGRectGetWidth(UIScreen.mainScreen().bounds) - horizonMergin, height: CGFloat.max)
        let options = unsafeBitCast(
            NSStringDrawingOptions.UsesLineFragmentOrigin.rawValue |
            NSStringDrawingOptions.UsesFontLeading.rawValue,
            NSStringDrawingOptions.self)
        
        // ここは必要に応じて
        var paragrahStyle = NSMutableParagraphStyle()
        paragrahStyle.lineHeightMultiple = 1.3
        paragrahStyle.lineSpacing = 4

        let font = UIFont.systemFontOfSize(14.0)
        var attributes = [NSFontAttributeName:font,
            NSParagraphStyleAttributeName:paragrahStyle]
        
        let frame = text.boundingRectWithSize(maxSize,
            options: options,
            attributes: attributes,
            context: nil)
        let height = ceil(frame.size.height) + verticalMergin
        
        return height
    }

NSAttributedString

    class func heightWithText(text: String) -> CGFloat {
        
        let horizonMergin:CGFloat = 32
        let verticalMergin:CGFloat = 32
        
        var attr = NSMutableAttributedString(string: text)
        
        var paragrahStyle = NSMutableParagraphStyle()
        paragrahStyle.lineHeightMultiple = 1.3
        paragrahStyle.lineSpacing = 4
        
        attr.addAttribute(NSParagraphStyleAttributeName, value: paragrahStyle, range: NSMakeRange(0, attr.length))
        
        
        let maxSize = CGSize(width: CGRectGetWidth(UIScreen.mainScreen().bounds) - horizonMergin, height: CGFloat.max)
        let options = unsafeBitCast(
            NSStringDrawingOptions.UsesLineFragmentOrigin.rawValue |
                NSStringDrawingOptions.UsesFontLeading.rawValue,
            NSStringDrawingOptions.self)
        
        let font = UIFont.systemFontOfSize(14.0)
        attr.addFontAttribute(font, range: NSRange(location: 0, length: attr.length))
        
        let frame = attr.boundingRectWithSize(maxSize,
            options: options,
            context: nil)
        let height = ceil(frame.size.height) + verticalMergin
        
        return height
    }

パフォーマンス

NSString : 0.000555038452148438 0.000557005405426025
NSAttributedString : 0.000142991542816162 0.000165998935699463

パフォーマンスはNSAttributedStringの方が早いです。

CoreDataのマルチスレッドのアクセスをチェックする

CoreDataのスレッド間のチェック


CoreDataはスレッドセーフではないため、ContextやManagedObjectがスレッドをまたぐ場合、場合によってはデッドロックになり、画面が固まったりします。
この場合によってはというのが曲者でなかなかその原因に気づかなかったりしてました。
ただiOS8.1 x Yosemiteからはフラグを設定すると解決できるみたいです。


-com.apple.CoreData.ConcurrencyDebug 1


実際に別スレッドで作ったContextをつかってオブジェクトを生成すると


エラーで止まってくれるので、間違った使い方をしててもすぐに発見できます!

もう少し早く知りたかった><

複数PCでsshのRSA鍵を使い回す

~/.ssh/にid_rsa_hogeとid_rsa_hoge.pubを置けばOKと思ってたけど、うまく認証されていなかったので、メモ的に残しておく

現在利用可能な鍵

ssh-addコマンドで表示される
ssh-add -l
2048 XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX /Users/hrk/.ssh/id_rsa (RSA)

他のPCで作成した鍵の追加

.ssh/配下に鍵を置く(別の場所でも大丈夫かも)
ssh-add -K .ssh/id_rsa_hoge
Identity added: id_rsa_hoge (id_rsa_hoge)
ファイルのパーミッションが600以外だとエラーがでます。
-Kをつけるとkeychainに保存されるため、PCを再起動しても再度読み込まれます。

Swiftで絵文字入りの文字列操作


文字列の扱い

Swiftで文字列はNSStringからStringクラスに変更された

文字数

これは結構ネットでもあるのでいいと思う
var str = "Hello"
var len = countElements(str) // 5

文字列の置換

x文字列目まで、x文字以降などを取得するsubstringToIndexsubstringFromIndexなどは注意が必要
(str as NSString).substringFromIndex(3)
絵文字がなければ特に問題ないのだが、絵文字が入るとうまく取得できない

advanceを使ってIndexを取得しそれを使う

var index = advance(str.startIndex, 3)
var str2 = str.substringToIndex(index)


UINavigationControllerをスワイプで遷移させてみる

ナビゲーションコントローラを滑らかに遷移させる

ナビゲーションコントローラの画面遷移をカスタマイズする

主要なクラス

UIViewControllerAnimatedTransitioning

具体的なアニメーションを定義するクラス。実行時間やviewの動きなど。

UIViewControllerInteractiveTransitioning

画面遷移の進捗を把握するクラス?途中経過や中止、完了などを教えてあげれば良きに計らってくれる。 UIPercentDrivenInteractiveTransitionを使うと楽

NavigationControllerDelegate

// pushやpopされたコントローラが渡されるので、適切なアニメーション定義クラスを返す
func navigationController(
  navigationController: UINavigationController,
  animationControllerForOperation operation: UINavigationControllerOperation,
  fromViewController fromVC: UIViewController,
  toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
}

// ここは何も気にせずにUIPercentDrivenInteractiveTransitionを返すが良い
func navigationController(navigationController: UINavigationController,
   interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
}

実装方法

今回はナビゲーションバーにおけるスワイプでのページ切り替えなので、NavigationControllerを継承したクラスでやってみる

アニメーションの定義

わかりやすく、PushとPopで分けて書く

class PushAnimatedTransitioning : NSObject, UIViewControllerAnimatedTransitioning {

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

        // 遷移元のVC
        var fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
        // 遷移先のVC
        var toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!

        // 表示中のView
        var containerView = transitionContext.containerView()


        var duration:NSTimeInterval = self.transitionDuration(transitionContext)



        // アニメーション終了時のframeを取得
        toViewController.view.frame = transitionContext.finalFrameForViewController(toViewController)

        // 右端から出すため初期値は幅分をプラスする
        toViewController.view.center.x += containerView.bounds.width

        containerView.addSubview(toViewController.view)


        UIView.animateWithDuration(duration,
            animations: { () -> Void in
                // 先ほどプラスした幅分を戻す
                toViewController.view.center.x -= containerView.bounds.width

            }, completion: { (Bool) -> Void in

                // キャンセルされていなければ完了
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled());
        })

    }

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return 0.3
    }

}

SwipeNavigationController

delegate

override func viewDidLoad() {
    super.viewDidLoad()

    self.delegate = self
}

// 画面遷移するときに使われるアニメーションを返す
func navigationController(navigationController: UINavigationController,
    animationControllerForOperation operation: UINavigationControllerOperation,
    fromViewController fromVC: UIViewController,
    toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        if operation == .Push {
            return PushAnimatedTransitioning()
        }
        return nil
}

// UIPercentDrivenInteractiveTransitionを返す
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return self.interactiveTransition
}

Pan gesture

UINavigationControllerのdelegateで設定。ここは特にどこでも良い

func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {

    var gesture = UIPanGestureRecognizer(target: self, action: "panGesture:")
    gesture.delegate = self
    viewController.view.addGestureRecognizer(gesture)
}

// 横スワイプのみ対応
func gestureRecognizerShouldBegin(gestureRecognizer: UIPanGestureRecognizer) -> Bool {
    var location = gestureRecognizer.translationInView(gestureRecognizer.view!)
    if self.nextViewController == nil { return false }
    return fabs(location.x) > fabs(location.y)
}

func panGesture(recognizer: UIPanGestureRecognizer) {
    var location = recognizer.translationInView(recognizer.view!)

    // どれくらい遷移したかを 0 〜 1で数値化
    var progress = fabs(location.x / (self.view.bounds.size.width * 1.0));
    progress = min(1.0, max(0.0, progress));

    // 次の画面が設定してなければ処理は継続しない
    if (self.nextViewController == nil) { return }

    if (recognizer.state == .Began) {
        // 左へのスワイプのみ
        if location.x > 0 { return }

        self.interactiveTransition = UIPercentDrivenInteractiveTransition()

        // ページ遷移させる!!!!!!
        self.pushViewController(self.nextViewController!, animated: true)
    }
    else if (recognizer.state == .Changed) {

        // 変化量を通知させる
        self.interactiveTransition?.updateInteractiveTransition(progress)
    }
    else if (recognizer.state == .Ended || recognizer.state == .Cancelled) {

        // 終了かキャンセルか
        if self.interactiveTransition != nil {
            if (progress > 0.5) {
                self.interactiveTransition?.finishInteractiveTransition()
                self.nextViewController = nil
            }
            else {
                self.interactiveTransition?.cancelInteractiveTransition()
            }
        }

        self.interactiveTransition = nil;
    }
}

使い方

storyboardなどでUINavigationControllerのClassをSwipeNavigationControllerにして、 スワイプで遷移させたいViewControllerを設定する。 callbackとかでもよかったけどちょっとめんどくさいかったので。。

    if let navi = self.navigationController as? SwipeNavigationController {
        navi.nextViewController = vc
    }

全体のコードはGithubで。

cocos2d-xのメモ置き場

anchorPoint (アンカーポイント)

(0.5f,0.5f)中央
(1,1)右上
(1,0)右下
(0,1)左上
(0,0)左下

左上に設置

node->setAnchorPoint(Vec2(0, 1));
node->setPosition(Vec2(0, bgSize.height));

左下に設置

node->setAnchorPoint(Vec2(0, 0));
node->setPosition(Vec2(0, 0));

左上に設置

node->setAnchorPoint(Vec2(1, 1));
node->setPosition(Vec2(bgSize.width, bgSize.height));

左下に設置

node->setAnchorPoint(Vec2(1, 0));
node->setPosition(Vec2(bgSize.width, 0));


ReactNativeでAndroid対応する話

前提 ReactNativeでiOS版のアプリをリリースしていて、Android版をリリースする話 トラブルシューティング Build.VERSION_CODES.Q が存在しないエラー compileSdkVersionを29以上にすると解決 メモリー足りないエラー Execu...