ラベル ios の投稿を表示しています。 すべての投稿を表示
ラベル ios の投稿を表示しています。 すべての投稿を表示

iOSシミュレータでURLスキームで起動する方法

URLスキームの実行方法

simctlがXcode6から付属しているのでそれを実行する

$ xcrun simctl openurl booted hoge:/foo/bar

simctl

シミュレータをコントロールできる

$ xcrun simctl
usage: simctl [--noxpc] [--set ] [--profiles ] ...
       simctl help [subcommand]
Command line utility to control the Simulator

For subcommands that require a argument, you may specify a device UDID
or the special "booted" string which will cause simctl to pick a booted device.
If multiple devices are booted when the "booted" device is selected, simctl
will choose one of them.

Subcommands:
        create              Create a new device.
        clone               Clone an existing device.
        upgrade             Upgrade a device to a newer runtime.
        delete              Delete a device or all unavailable devices.
        pair                Create a new watch and phone pair.
        unpair              Unpair a watch and phone pair.
        pair_activate       Set a given pair as active.
        erase               Erase a device's contents and settings.
        boot                Boot a device.
        shutdown            Shutdown a device.
        rename              Rename a device.
        getenv              Print an environment variable from a running device.
        openurl             Open a URL in a device.
        addmedia            Add photos, live photos, or videos to the photo library of a device.
        install             Install an app on a device.
        uninstall           Uninstall an app from a device.
        get_app_container   Print the path of the installed app's container
        launch              Launch an application by identifier on a device.
        terminate           Terminate an application by identifier on a device.
        spawn               Spawn a process on a device.
        list                List available devices, device types, runtimes, or device pairs.
        icloud_sync         Trigger iCloud sync on a device.
        pbsync              Sync the pasteboard content from one pasteboard to another.
        pbcopy              Copy standard input onto the device pasteboard.
        pbpaste             Print the contents of the device's pasteboard to standard output.
        help                Prints the usage for a given subcommand.
        io                  Set up a device IO operation.
        diagnose            Collect diagnostic information and logs.
        logverbose          enable or disable verbose logging for a device

デバイス確認

$ xcrun simctl list
== Device Types ==
iPhone 4s (com.apple.CoreSimulator.SimDeviceType.iPhone-4s)
iPhone 5 (com.apple.CoreSimulator.SimDeviceType.iPhone-5)
iPhone 5s (com.apple.CoreSimulator.SimDeviceType.iPhone-5s)
・・・
== Runtimes ==
iOS 9.3 (9.3 - 13E233) - com.apple.CoreSimulator.SimRuntime.iOS-9-3
iOS 10.3 (10.3.1 - 14E8301) - com.apple.CoreSimulator.SimRuntime.iOS-10-3
iOS 11.2 (11.2 - 15C107) - com.apple.CoreSimulator.SimRuntime.iOS-11-2
tvOS 11.2 (11.2 - 15K104) - com.apple.CoreSimulator.SimRuntime.tvOS-11-2
watchOS 4.2 (4.2 - 15S100) - com.apple.CoreSimulator.SimRuntime.watchOS-4-2
== Devices ==
-- iOS 9.3 --
    iPhone 4s (269EF856-87D0-41E1-B303-CE36A65E712E) (Shutdown)
    iPhone 5 (35804246-54E9-47BC-AFC2-83E639744F7B) (Shutdown)
    iPhone 5s (AFAA2A65-FB92-441B-820F-CE7C97BE6AF1) (Shutdown)
    iPhone 6 (98529E42-EE5A-404F-B476-27C61737AD12) (Shutdown)
・・・
-- iOS 11.2 --
    iPhone 5s (A698838A-704E-43C6-AE8B-1E81AB0E1311) (Shutdown)
    iPhone 6 (6412A351-97A7-49A1-B860-A51EDB631C48) (Booted)
    iPhone 6 Plus (8AD29BA9-06A5-4036-BA95-477D05FE30AB) (Shutdown)
    iPhone 6s (6048B53E-87FF-429C-A5A1-5BE62CB8C2F4) (Shutdown)

Open URL

$ xcrun simctl openurl
Usage: simctl openurl

deviceとURLを指定する

deviceは先ほどしらべたUUIDでも良いし、bootedと指定しても良い

iOS11アプリ起動中にPush通知が受け取れない

iOS10からUserNotificationが導入されて、RemoteNotificationと共存している時に、iOS11ではアプリがForegroundにいる時に通知が取得できない状態になっていた。


```
  func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
     // ここで処理を行う

  }
```

iOS10までは、UserNotificationも上記が呼ばれる

iOS11ではUNUserNotificationCenterDelegateを実装しないと呼ばれない

```
    @available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

      // ここで処理を行う

      completionHandler([])

    }
```

アプリ起動時もUserNotificationを表示させたい場合はcompetionHandler([.badge, .alert, .sound])とすると、表示される。

そのをたっぷすると、上のdidReceiveRemoteNotificationあ呼ばれる

fastlaneでデバイス登録

Fastlaneを使ってデバイスの登録を行い、Provisioning Profileへの追加、Xcodeの更新を行う

デバイスの追加

register_devicesで追加

Provisioning Profileの更新

sigh で更新

adhocやdevelopmentの指定が可能

```
    # Adhoc
    sigh(force: true, adhoc: true)
    # Development
    sigh(force: true, development: true)
```

Xcodeの設定の更新

update_project_provisioningで更新

ビルドの指定や、ファイル名の指定が可能

```
    update_project_provisioning(build_configuration: "Debug")
```


現時点でのFastfaile



```
  desc "Register Device"
  lane :add_device do |options|
    if options[:name] && options[:udid]
      register_devices(devices: {options[:name] => options[:udid]})
      update_develop_provisioning()
      update_adhoc_provisioning()
    else
      UI.error "Usage: fastlane add_device name:'New device name' udid:'UDID'"
    end
  end

  desc "update developmemt provisioning profile"
  lane :update_develop_provisioning do
    sigh(
      force: true,
      development: true,
      output_path: "provisioning",
      filename: "development.mobileprovision"
    )
    update_project_provisioning(
      build_configuration: "Debug",
      profile: "provisioning/development.mobileprovision",
    )
  end

  desc "update adhoc provisioning profile"
  lane :update_adhoc_provisioning do
    sigh(
      force: true,
      adhoc: true,
      output_path: "provisioning",
      filename: "adhoc.mobileprovision"
    )
    update_project_provisioning(
      build_configuration: "Release",
      profile: "provisioning/adhoc.mobileprovision",
    )
  end
```

使い方
fastlane add_device name:'hrk iPhone7' udid:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx





iOS circleciのビルド時間を1/3にした話

前提


CircleCIの実行時間は17分から18分
主に時間がかかっているところは

checkout 1分
pod install 5分
build+upload(fabric beta) 9分


やったこと

前からやってみたかったcocoapodsをgit管理にする

変更前

.gitignore
    Pods/
    *.xcworkspace/


変更後

.gitignore
    # Pods/
    # *.xcworkspace/
    !Pods/**/vendor/


一部bundlerを使ってたので、vendorの除外を追加

結果


checkout 1分20秒
pod install なし
build+upload(fabric beta) 9分


Podsのコード分だけ、checkoutが時間かかるけど、5分かかってたのが20秒に短縮されたのは大きい。

あとはgit cloneしてそのままビルドできるところも良いかも。
cocoapodsのバージョンが違うとか、気にしなくて良いので。

Fabric bataへアプリをアップロードする

プロジェクトに直接インストールしている場合


./Crashlytics.framework/submit API_KEY BUILD_SECRET -ipaPath IAP_PATH

CocoaPodsを使ってインストールしている場合


./Pods/Crashlytics/submit  API_KEY BUILD_SECRET -ipaPath IAP_PATH


その他のオプション
     Usage:
        submit  API_KEY BUILD_SECRET        default options for upload after archive, with ipa path in environment
         additional options:
         -help                          display this message
         -ipaPath           [path to IPA]
         -emails            [tester email address],[email]
         -groupAliases      [group build server alias],[group]
         -notesPath         [release notes]
         -notifications     YES|NO
         -debug             YES|NO

Cocoapods 1.0.0にアップデートした時の対応

ターゲットの指定が必須

0.39.0の記述

platform :ios, "8.0"
use_frameworks!

pod 'Google/Analytics'
pod 'AFNetworking'
pod 'SSKeychain'
pod 'SVProgressHUD'


1.0.0の記述

platform :ios, "8.0"
use_frameworks!

target "Sample" do
  pod 'Google/Analytics'
  pod 'AFNetworking'
  pod 'SSKeychain'
  pod 'SVProgressHUD'
end



Acknowledgementsファイルのパスの変更

0.39.0
FileUtils.cp_r('Pods/Target Support Files/Pods-Sample/Pods-Sample-acknowledgements.plist', 'Sample/Settings.bundle/Acknowledgements.plist', :remove_destination => true)

1.0.0
FileUtils.cp_r('Pods/Target Support Files/Pods/Pods-acknowledgements.plist', 'Sample/Settings.bundle/Acknowledgements.plist', :remove_destination => true)

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をつかってオブジェクトを生成すると


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

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

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で。

iOS8のNotification関係を調べてみた


What’s New in iOS Notifications

WWDC 2014のVideo Sessionを見てみる。 https://developer.apple.com/videos/wwdc/2014/

User Notification iOS7の説明

User Notificationsとは

  • Alert表示
  • Notification Centerに通知
  • スクリーンOFFの時に表示

User Notificationの実行

  • アプリからLocal Notificationを使う
  • APNsからRemote Notificationを使う
  • APNsからcontent-available: 1を指定してappを介して通信する

User Notifications iOS8

今回紹介するのは大きく以下の4つ
  • User Notifications
  • Notification Actions
  • Remote Notification
  • Location Notification

User Notifications

登録方法

UIRemoteNotificationTypeと同じようなイメージ
categoryという概念が追加されているが後ほど
UIUserNotificationType types = UIUserNotificationTypeBadge |
  UIUserNotificationTypeSound | UIUserNotificationTypeAlert;

UIUserNotificationSettings *mySettings = [UIUserNotificationSettings
  settingsForTypes:types categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];

ユーザパーミッション

UIApplicationDelegateにCallbackメソッドがある
許可しない場合は、allowedTypes == UIUserNotificationTypeNoneとなる
 - (void)application:(UIApplication *)application
     didRegisterUserNotificationSettings:
    (UIUserNotificationSettings *)notificationSettings {

    // user has allowed receiving user notifications of the following types
    UIUserNotificationType allowedTypes = [notificationSettings types];
}

許可されたタイプの取得

- (void)getReadyForNotification {
  // ...
  // check to make sure we still need to show notification
  UIUserNotificationSettings *currentSettings = [[UIApplication
   sharedApplication] currentUserNotificationSettings];
  [self checkSettings:currentSettings];
}

Notification Action

iOS7ではそれぞれのNotificationに対して、スワイプで削除、タップで起動しかできなかった
iOS8ではスワイプして、複数のボタンを準備してアクションさせる事が可能




Actionの作成方法

ボタンのタイトルやバックグランド、ロック解除前でも可能かどうかを定義する
UIMutableUserNotificationAction *acceptAction =
  [[UIMutableUserNotificationAction alloc] init];

acceptAction.identifier = @"ACCEPT_IDENTIFIER";

acceptAction.title = @"Accept";

// Given seconds, not minutes, to run in the background
// UIUserNotificationActivationModeForegroundを指定すると、選択時にアプリを起動する
acceptAction.activationMode = UIUserNotificationActivationModeBackground;

acceptAction.destructive = NO;

// If YES requires passcode, but does not unlock the device
acceptAction.authenticationRequired = NO;

Categoryの作成方法

複数のアクションを登録したカテゴリを作成する
例えば: - メールカテゴリだと、返信アクション、アーカイブアクションなど登録 - 友達申請用のカテゴリだと、承認アクション、保留アクションなど登録
UIMutableUserNotificationCategory *inviteCategory =
  [[UIMutableUserNotificationCategory alloc] init];

inviteCategory.identifier = @"INVITE_CATEGORY";

[inviteCategory setActions:@[acceptAction, maybeAction, declineAction]
  forContext:UIUserNotificationActionContextDefault];
UIUserNotificationActionContextDefault ボタンのサイズが普通 UIUserNotificationActionContextMinimal ボタンのサイズが若干小さめ

定義したUserNotificationの登録

UIApplicationにActionを登録したCategoryを設定したSettingを登録する
NSSet *categories = [NSSet setWithObjects:inviteCategory, alarmCategory, ...

UIUserNotificationSettings *settings =
  [UIUserNotificationSettings settingsForTypes:types categories:categories];

[[UIApplication sharedApplication]
  registerUserNotificationSettings:settings];

カテゴリを指定したNotificationの発行

Remote Notification

apsにcategoryを追加して飛ばす
{
  "aps" : {
    "alert" : "You’re invited!",
    "category" : "INVITE_CATEGORY",
  }
}

Local Notification

UILocalNotification *notification = [[UILocalNotification alloc] init];

...
notification.category = @"INVITE_CATEGORY";

[[UIApplication sharedApplication] scheduleLocalNotification:notification];

Handling Notification Action

iOS7ではアプリが起動して無い場合
application:didFinishLaunchingWithOptions:
application:didReceiveRemoteNotification:fetchCompletionHandler:
アプリが起動している場合は
application:didReceiveLocalNotification:
application:didReceiveRemoteNotification:
application:didReceiveRemoteNotification:fetchCompletionHandler:
iOS8では
// Push Notificationの場合
- (void)application:(UIApplication *)application
  handleActionWithIdentifier:(NSString *)identifier
     forRemoteNotification:(NSDictionary *)notification
         completionHandler:(void (^)())completionHandler {
    if ([identifier isEqualToString:@"ACCEPT_IDENTIFIER"]) {
      [self handleAcceptActionWithNotification:notification];
    }
    // Must be called when finished
    completionHandler();
}

// Local Notificationの場合
- (void)application:(UIApplication *)application
   handleActionWithIdentifier:(NSString *)identifier
  forLocalNotification:(UILocalNotification *)notification
   completionHandler:(void(^)())completionHandler {

}
を使う。identifireにActionで選択されたIDが入ってくる。

Remote Notifications

User
  • Requires call to registerUserNotificationSettings:
Silent
  • Info.plist UIBackgroundModes array contains remote-notification

登録方法

×[myApp registerForRemoteNotificationTypes:someTypes]; [myApp registerForRemoteNotifications]; [myApp registerUserNotificationSettings:mySettings];

Location Notifications

一定エリアに入ったタイミング、出るタイミング
一定エリアに入ったら毎回通知するか、一回だけ通知するか選択可能
以前もあったけどシンプルに実装できるようになったイメージ

使い方

UILocalNotification * locNotification;

locNotification.regionTriggersOnce = YES;

locNotification.region = [[CLCircularRegion alloc] initWithCenter:LOC_COORDINATE
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];

Facebook Tweaksを使ってみた

Tweaksとは

iOSアプリ開発において、UXなどインタラクティブな操作において、
微妙なパラメータ調整を行いたい場合、簡単に設定画面を作って、再度ビルドすることなく、
値を変更して動作確認するためのライブラリです。(たぶん)

FacebookのPaperでも使われているそうです。

Github

https://github.com/facebook/Tweaks

インストール

pod 'Tweaks'

使い方

値の取得

引数は、カテゴリ、コレクション、名前、値(複数)

CGFloat animationDuration = FBTweakValue(@"Category", @"Group", @"Duration", 0.5);

※リリースビルドではデフォルト値を展開するだけなので、パフォーマンス的に問題にはならない

最小、最大数

数字に対しては、値を複数していして、デフォルト値、最小値、最大値を指定することができる

self.red = FBTweakValue(@"Header", @"Colors", @"Red", 0.5, 0.0, 1.0);

バインド

微調整のパラメータが変更されたら自動で更新されます。
第一引数に対象のオブジェクト、第2引数にプロパティーを指定します。

FBTweakBind(self.headerView, alpha, @"Main Screen", @"Header", @"Alpha", 0.85);

アクション

微調整リストを選択したときの処理をBlockを使って定義する事ができる。
ただし、block内はグローバルスコープになる

FBTweakAction(@"Player", @"Audio", @"Volume", ^{
  NSLog(@"Action selected.");
});

設定画面

設定画面を表示する方法2種類

シェイクジェスチャーで表示

AppDelegate.mに以下を追加

- (UIWindow *)window
{
    if (!_window) {
        _window = [[FBTweakShakeWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    }

    return _window;
}

シミュレータでシェイクジェスチャをする場合は⌘Z

FBTweakViewControllerをモーダルで呼ぶ

モーダルで呼び出す。FBTweakViewController自体がNavigation Controllerなので、pushするとクラッシュする

FBTweakViewController* vc = [[FBTweakViewController alloc] initWithStore:[FBTweakStore sharedInstance]];
[self presentViewController:vc animated:YES completion:nil];

その他

マクロを使わずに直接オブジェクトを生成する事も可能

FBTweak *tweak = [[FBTweak alloc] initWithIdentifier:@"com.tweaks.example.advanced"];
tweak.name = @"Advanced Settings";
tweak.defaultValue = @NO;

FBTweakStore *store = [FBTweakStore sharedInstance];
FBTweakCategory *category = [store tweakCategoryWithName:@"Settings"];
FBTweakCollection *collection = [category tweakCollectionWithName:@"Enable"];
[collection addTweak:tweak];

[tweak addObserver:self];

変更通知

- (void)tweakDidChange:(FBTweak *)tweak
{
  self.advancedSettingsEnabled = ![tweak.currentValue boolValue];
}

まとめ

ちょっとパラメータで調整したいなーと思う事はこったUIを作る場合や、複数人(デザイナーさんがいる)場合に遭遇する事があります。そのときにわざわざ設定画面を作る事無くマクロ一つで作成できるのはものすごーく魅力的に思います。

いざリリースするときにコードにゴミが残るのが少し気になりそうですが、実際に使ってみてある程度パラメータが固まったら削除するすれば良いし、このライブラリが無ければ、都度ビルドするとなるとそっちの方が行けてないなーと思いました。

iOS FAQサポートツール Helpshiftを使ってみた


リンク集

アカウント作成

本家ページからアカウント作成を行う。
既にアプリを公開している場合は、App StoreのURLを入れるとアプリ情報を参照してくれる

インストール

pod 'Helpshift', '4.2.0'    

導入

初期処理

他のサービス同様にアプリ起動時にAPI Key、APP IDを指定する
Push通知は使わなければ設定しなくても良い
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // ... 省略

    [Helpshift installForApiKey:@"<YOUR_API_KEY>"  domainName:@"<YOUR_COMPANY>.helpshift.com" appID:@"<YOUR_APP_ID>"];


    if (launchOptions != nil) //handle when app is not in background and opened for push notification.
    {
        NSDictionary* userInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
        if (userInfo != nil && [[userInfo objectForKey:@"origin"] isEqualToString:@"helpshift"])
        {
            [[Helpshift sharedInstance] handleRemoteNotification:userInfo withController:self.window.rootViewController];
        }
    }
}

- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    if (!deviceToken) return;

    [[Helpshift sharedInstance] registerDeviceToken:deviceToken];
}



- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    //Helpshift::handle notification from APN
    if ([[userInfo objectForKey:@"origin"] isEqualToString:@"helpshift"]) {
        [[Helpshift sharedInstance] handleRemoteNotification:userInfo withController:self.window.rootViewController];
    }
}

問い合わせ機能

お問い合わせ機能では、基本的にユーザとのメッセージのやり取りの他に、スナップショットを添付させたり、レビュー依頼を出せたりする。
[[Helpshift sharedInstance] showConversation:self withOptions:nil];
メッセージ投稿フォームが起動する

どうやらNameの所は数字とか入力できないもよう。
設定で名前やメールアドレスの入力フォームを非表示にできる
SETTINGS > Allow anonymous issues

OPE

ISSUES
問い合わせされた内容が表示される
  • ステータス管理
  • タグ機能
  • メモ
  • 問い合わせをベースにFAQを作成する事も可能
  • キャプチャーを撮ってもらうように促して、キャプチャーを添付してもらう事も可能
  • Push通知
  • アプリ内通知(アプリを開いてるとき)
  • レビュー依頼
ステータス
ユーザの問い合わせでステータスがオープンになり、その後メッセージのやり取りが行われる。 最後はユーザ側のアクションでCloseとなる
  1. open
  2. メッセージをやり取り
  3. オーペレータがResolvedに変更
  4. ユーザがCloseする

FAQ

[[Helpshift sharedInstance] showFAQs:self withOptions:nil];

OPE

特に困る事はないけど、新規作成したときは「非公開」になってるので、公開にする

国際化

HSLocalizationフォルダを追加する
Pods/Helpshift/helpshift-ios-4.2.0/HSLocalization
10種類の言語ファイルがあるので、必要なやつだけ追加する。
自分の場合はEnglishだけで、日本語を表示してるので、英語意外は全部削除。
そして、日本語の内容をコピペする。(やはり日本語辺なので修正した方が良さそう)

デザイン

HelpshiftConfig.plist もしくは HelpshiftConfigDark.plist を追加する
HelpshiftConfigDark.plistを追加する場合は、ファイル名を’HelpshiftConfig.plist'に変更する
Pods/Helpshift/helpshift-ios-4.2.0/HSThemes
全体的なデザインはデフォルトのままなので、あまり気にならないが、フォントだけどうしてもきになったので、 すべてヒラギノに変更
  • ヒラギノ角ゴ ProN W3
  • ヒラギノ角ゴ ProN W6

アプリの評価

OPEのSETTINGSからアプリの評価を促すダイアログの表示条件を指定できる
  • 5, 10, 15, 20, 30回目の起動
  • 3, 7, 10, 15, 20, 30日後の起動


実際に導入したアプリはこちら

DeskSlide

スマホとPC間のデータ転送が簡単に行えます。
URLなどのテキストや、画像ファイルなどが転送できるアプリです。

ReactNativeでAndroid対応する話

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