The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

Fetch APIを使ってRailsのAPIを叩く

JavaScript(es2015)でFetch APIを使ってRailsAPIを呼ぶときの方法。

ポイントとしては、'credentials: same-origin'をつけることと、 CSRFを有効にしている場合はheaderにX-CSRF-Tokenをつける点です。

    const getCsrfToken = () => {
        const metas = document.getElementsByTagName('meta');
        for (let meta of metas) {
            if (meta.getAttribute('name') === 'csrf-token') {
                console.log(meta.getAttribute('content'));
                return meta.getAttribute('content');
            }
        }
        return '';
    }

    const postFoo = () => {
        return fetch('/api/foo', {
            method: 'POST',
            credentials: 'same-origin',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': getCsrfToken()
            },
            body: JSON.stringify({
                bar: "bar",
            })
        }).then((response) => {
            if (response.status >= 200 && response.status < 300) {
                return response.json();
            } else {
                const error = new Error(response.statusText);
                throw error;
            }
        });
    }

iOSのFrameworkのVersionについて

Frameworkにはversionがいくつかあってそれらの違いの雑な説明です。

その1

ひとつはお馴染みのinfo.plistに書くやつ これはちょっと省きます

その2

Umbrella Headerに書いてあるやつ

FOUNDATION_EXPORT double FwVersionNumber;
FOUNDATION_EXPORT const unsigned char FwVersionString[];

Build SettingsのCurrent Project Versionがこれにあたります

Frameworkをコンパイルするとわかるんですが、 (ここでは例としてFwというframeworkをつくります)

Compile Fw_vers.c

というcのファイルが勝手に生成されてコンパイルされてます。 中身を見てみると

 extern const unsigned char FwVersionString[];
 extern const double FwVersionNumber;

 const unsigned char FwVersionString[] __attribute__ ((used)) = "@(#)PROGRAM:Fw  PROJECT:Fw-10" "\n";
 const double FwVersionNumber __attribute__ ((used)) = (double)10.;

とこんな感じでxxVersionNumberとxxVersionStringが定義されてます。

Umbrella Headerではこれらをexternしているので、Frameworkを利用しているプログラムはこれらの変数を使うことができます。

その3

Framework VersionのAとかはFrameworkの中身のフォルダ構成を見ての通りでCurrentさしているディレクトリになります

Anatomy of Framework Bundles

その4

current library versionとcompatibility versionについてですが、

Link時のビルドログをみるとわかるようにこれらの値はclangのオプションとして渡されて使われる感じになります。

clang ...  -compatibility_version 1 -current_version 1

詳しくはこの辺を読むと良いのではないでしょうか

developer.apple.com

現場からは以上です。

AutolayoutでレイアウトしたViewを外して元に戻す

InterfaceBuilderやStoryboardでAutolayoutを使って配置したViewをremoveFromSuperviewしてから、 再度addSubviewしたい!ということがたまにあります。

普通にremoveFromSuperviewしてaddSubviewすると元には戻りません。 なぜならremoveFromSuperviewした時点で親と消された子のViewのAutolayoutの制約は消えてしまうからです。

どうすればよいかというremoveFromSuperviewする前に制約をどこかに保存しておき、 addSubview後にそれらの制約を追加すればよいです。

view以下にbuttonが配置されていたのを取ったりつけたりする場合です。

private var buttonConstraints = [NSLayoutConstraint]()

...

// removeFromSuperviewするとき
view.constraints.forEach {
    if $0.firstItem is UIButton || $0.secondItem is UIButton {
        self.buttonConstraints.append($0)
    }
}
button.removeFromSuperview()

...
// addSubviewするとき
view.addSubview(button)
buttonConstraints.forEach {
    view.addConstraint($0)
}

独自のNotification名を追加

独自のNotification名を追加する場合はこんな書き方がよいような気がします。

extension Notification.Name {
    struct AppName {
        public static let DidLogin = Notification.Name(rawValue: "com.example.appname.didLogin")
    }
}

通知名の文字列には通知名の衝突を避けるために独自のprefixをつけています。

com.example.appnameのところはBundle.main.bundleIdentifierでbundle identifierをとってきて 使ったりするのがよいかもしれません。

CircleCIでipaファイルを作ろうとするとExport Failedする

ローカルではarchiveからipaにexportできるのにCircleCIだとExport Failedになる場合の対処法です。

原因としてはCircleCIの環境変数が悪さをしているようで、以下のをunsetすればなおりました

unset BUNDLE_BIN_PATH
unset BUNDLE_GEMFILE
unset BUNDLE_ORIG_PATH
unset GEM_HOME
unset GEM_PATH
unset RUBYLIB
unset RUBYOPT

面倒な場合は、このxcode-safe.shを使うと良いです。

xcode-safe.sh

https://gist.github.com/claybridges/cea5d4afd24eda268164

Visual Studio Codeをemacs keybindingにする

Visual Studio Codeを使うときにEmacs Keybindingにする方法です。

Emacs Keymap(Emacs Keymap - Visual Studio Marketplace)というのが存在するので、これを使ってみます。

  1. Shift + Command + Xを押して拡張機能を開きます
  2. 検索窓でemacsといれるとEmacs Keymapというのがでてきますので、インストールを押せばOKです。

Macで出たエラー

MacC-x C-fでファイルを開こうとすると以下のようなエラーがでました

command 'workbench.action.files.openFolder' not found

↓を見た感じ、Macだと'openFileFolder'じゃなきゃアカン的なことが書いてありました・・

bind command(workbench.action.files.openFile) failed · Issue #5437 · Microsoft/vscode · GitHub

メニューから Code->基本設定->キーボード ショートカット を選択し、 以下のようにキーバインドを上書きするとなおりました。

// 既定値を上書きするには、このファイル内にキー バインドを挿入します
[
    {
        "key": "ctrl+x ctrl+f",
        "command": "workbench.action.files.openFileFolder"
    }
]

SwiftでKVOするときはObjective-Cのプロパティ名を使う

当たり前といえば当たり前なんですが、 iOSのKVO(addObserver(_:forKeyPath:options:context:))はObjective-CのNSObjectのメソッドなので、指定するkeyPathはObjective-Cのプロパティ名じゃないとだめです。

例えばUIViewのisHiddenをKVOしたい場合は↓のように、isHiddenではなくhiddenになります。

// Swift3以前
view.addObserver(self, forKeyPath: "hidden", options: [], context: nil)
// Swift3以降
view.addObserver(self, forKeyPath: #keyPath(UISwitch.hidden), options: [], context: nil)

iOSフレームワークのクラスに対してKVOを使う際は、補完に頼るとミスることがあるので、Objective-Cの仕様も確認しましょう (#keyPathがもうちょい賢ければ・・)

addObserver(_:forKeyPath:options:context:) - NSObject | Apple Developer Documentation

TwitterKitを使ってログインするときの注意点

TwitterKitを使ってログインするときの注意点を2点

1. Twitterアカウントを登録しているかで挙動が変わる

TwitterKitを使ってTwitterログインするときに、TwitterのアカウントをiOSに設定しているかどうかで挙動が変わります。 (2017年1月時点)

iOSの設定でTwitterアカウントを登録している場合

iOSTwitterアカウントを登録済の状態でTwitterログインをすると、許可のポップアップが最初に表示されます。

ここで許可すると、Twitterログインが成功します。(特に画面遷移が発生しない)

iOSの設定でTwitterアカウントを登録していない場合

SafariViewControllerが立ち上がり、Twitterログイン画面が表示されます。


といった風に遷移が変わってくるので、動作確認する場合は、2パターンは確認する必要があります。

動作確認時に発生した不具合

Twitterアカウントを登録していない場合はSafariViewControllerが立ち上がりますが、以下のようなエラーがコンソールに表示されてSafariViewControllerが立ち上がらないことがあります。

Desktop applications only support the oauth_callback value 'oob'

対応方法としては、 Twitter Application Management で、callback URLになにかしらURLを設定すると治ります。

2. Info.plistにconsumer keyとsecretが書かれる

Fabricを使ってTwitterKitを取り込むとInfo.plistにconsumer keyとconsumer secretが書かれます。

アプリ側で保持している以上どこに書いても問題はあるとは思うのですが、Info.plistに書くよりはコード上に書いたほうが幾分ましかなと思います。

コードでConsumerKeyを設定する

このようにFabric.with([Twitter.self])より前にstartWithConsumerKeyを呼んでやらないとクラッシュします。

import TwitterKit

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        ...
        Twitter.sharedInstance().start(withConsumerKey: "xxx", consumerSecret: "yyy")
        Fabric.with([Twitter.self])
        return true
    }

2016振り返り

2016も残りわずかとなったところで急いで今年を振り返ります!

仕事

1月にLIMIAのiPhoneアプリをリリースしました。 フルSwiftで開発しました。

2月にはAppStoreの新着ベストアプリにも選ばれました!

3年半ほどお世話になったグリーを6月に退社してSupershipにジョインしました。

開発の途中から参加してものすごい勢いで実装して7月にはSunnychatのiPhoneアプリをリリースしました。 こちらもSwiftです。

プライベート

こどもが産まれまして、ワークライフバランスがこれまでと著しく変わってきました。

今はとにかく自分の時間がとれなくて勉強するのがなかなかたいへんです。

勉強

Elixirを勉強しましたけどコーディングは全然です

ランニング

10月まではほぼ毎週走ってたんですが、こどもが産まれてそれどころではなくなったので最近は全く走れてないです。。

f:id:yanamura:20161231232555p:plain

まとめ

2016は仕事、プライベートともに激動の年だったなぁと。

2017は子育てを頑張りつつ、自分の時間もうまくつくってなにかやっていきたいですね

Swiftでの複数にデリゲート multicast delegate

Swiftにはweak reference arrayがないので、NSHashTableを利用します

protocol SampleDelegate : class {
    func sampleDelegateDidFinish()
}
class SampleClass {
    let delegates = NSHashTable<AnyObject>() // AnyObjectをSampleDelegateにするとSwift3時点ではコンパイルに通らない

delegateを呼ぶとき

func someFunc() {
...
            delegates.objectEnumerator().enumerated()
                .map { $0.element as? SampleDelegate }
                .forEach { $0?.sampleDelegateDidFinish()) }
...
}

Xcode8.2からコマンドラインでシミュレータの動画が撮れる

Xcode8.2でコマンドラインでシミュレータの動画が撮れるようになって便利に

スクリーンショット

xcrun simctl io booted screenshot. To take a video, run the command 

動画

xcrun simctl io booted recordVideo <filename>.<file extension>

実行すると

Recording... (Press CTL+C to stop)

と表示されるので止めたいときはCTL+Cで停止させます。

popToRootViewControllerで画面を消すとviewWillDisappearでnavigationControllerがnilになる

現象が伝えにくいので、図に表すと以下のような感じで、 TabbarController内にNavigationControllerをもたせた状態で、いくつかViewControllerをPushViewControllerします。 そして、2つ以上PushViewControllerした状態で一番上のViewControllerのナビバーを透明にした場合にちょっと問題が発生します。

f:id:yanamura:20161208104925p:plain

一番上のViewControllerでは以下のようにして、ナビバーを透明にして、消すときにナビバーを元に戻す用にしています。

class SampleViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
        navigationController?.navigationBar.shadowImage = UIImage()
        navigationController?.navigationBar.isTranslucent = true
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        navigationController?.navigationBar.shadowImage = nil
        navigationController?.navigationBar.isTranslucent = false
    }
}

普通に戻るボタンを押した場合はこれで正常に動作するのですが、 タブバーのボタンをタップしてrootviewcontrollerに戻った場合に、viewWillDisappearが呼ばれたときにはnavigationControllerがnilになっているため、ナビバーが透明なままになってしまうという問題が発生しました。。

対策

その1

やっつけ仕事感のある対応としてはnavigationControllerを自力で保持するというやりかたで、、

    private weak var toKeepNavigationController: UINavigationController?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        toKeepNavigationController = navigationController

        navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
        navigationController?.navigationBar.shadowImage = UIImage()
        navigationController?.navigationBar.isTranslucent = true
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        toKeepNavigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        toKeepNavigationController?.navigationBar.shadowImage = nil
        toKeepNavigationController?.navigationBar.isTranslucent = false
    }

その2

willMoveToParentViewControllerの時点ではnavigationControllerはnilになっていないようだったので、ViewControllerが消えるタイミング(parentがnilになったとき)にナビバーをもとにもどします

    override func willMove(toParentViewController parent: UIViewController?) {
        super.willMove(toParentViewController: parent)
        if parent == nil {
            navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
            navigationController?.navigationBar.shadowImage = nil
            navigationController?.navigationBar.isTranslucent = false
        }
    }

たぶん2のほうがいいと思います!

おかしいぞとかあればツッコミお願いします!

Swift3.0.1で若干変わったIntなどの数値型⇔AnyObjectのcast

Xcode8(Swift3.0.0)だと↓のテストは通るんですが、

import XCTest
@testable import TypeTest

class TypeTests: XCTestCase {
    
    func testType_WhenIntToAnyObject() {
        let intValue = ["hoge": Int(1) as AnyObject]

        XCTAssertTrue(intValue["hoge"] is Int)
        XCTAssertTrue(intValue["hoge"] is Float)
        XCTAssertTrue(intValue["hoge"] is Double)
        XCTAssertTrue(intValue["hoge"] is CGFloat)
    }

    func testType_WhenCGFloatToAnyObject() {
        let cgfloatValue = ["hoge": CGFloat(1.0) as AnyObject]

        XCTAssertTrue(cgfloatValue["hoge"] is Float)
        XCTAssertTrue(cgfloatalue["hoge"] is Double)
        XCTAssertTrue(cgfloatValue["hoge"] is CGFloat)
    }
}

Xcode8.1(Swift3.0.1)だと↓のようにAnyObjectにcastする前の型のみ正しく判定されるようになってました f:id:yanamura:20161109100647p:plain

SE-0139によるところかと思われます。 あんまりちゃんと読んでないですが、以下のようなこと書いてあったし。

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types.

雑にAnyObjectをcast backしてる場合はSwfit3.0.1にするとcastに失敗するようになるので注意が必要かもです

CGRectの新旧書き方対応表

CGRectGetWidth( rect ) rect.width
CGRectGetHeight( rect ) rect.height
CGRectGetMinX( rect ) rect.minX
CGRectGetMidX( rect ) rect.midX
CGRectGetMaxX( rect ) rect.maxX
CGRectGetMinY( rect ) rect.minY
CGRectGetMidY( rect ) rect.midY
CGRectGetMaxY( rect ) rect.maxY
CGRectIsNull( rect ) rect.isNull
CGRectIsEmpty( rect ) rect.isEmpty
CGRectIsInfinite( rect ) rect.isInfinite
CGRectStandardize( rect ) rect.standardize
CGRectIntegral( rect ) rect.integral
CGRectInset(rect, 1.0, -2.0) rect.insetBy(dx: 1.0, dy: -2.0)
CGRectOffset(rect, -1.0, 2.0) rect.offsetBy(dx: -1.0, dy: 2.0)
CGRectUnion(rect1, rect2) rect1.union(rect2)
CGRectIntersection( rect1 ,rect2) rect1.intersect(rect2)
CGRectContainsRect( rect1,rect2) rect1.contains(rect2)
CGRectContainsPoint(rect ,point) rect.contains(point)
CGRectIntersectsRect( rect1,rect2 ) rect1.intersects(rect2)

Xcode8にしたときのプッシュ通知対応

Project SettingsのCapabilitiesのタブを開き、PushNotificationsをONにします

f:id:yanamura:20160916183405p:plain

production.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>production</string>
</dict>
</plist>

development.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>development</string>
    <key>keychain-access-groups</key>
    <array>
        <string>$(AppIdentifierPrefix)share</string>
    </array>
</dict>
</plist>

Build SettingsのCode Signing Entitlementsで、Configurationによって使うentitlementsを変えるようにします

f:id:yanamura:20160916183853p:plain

最後にこれは必須ではないですが、

        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptions([.Alert, .Sound, .Badge]) { granted, _ in
                if granted {
                    UIApplication.sharedApplication().registerForRemoteNotifications()
                }
            }
        } else {
            let settings = UIUserNotificationSettings(forTypes: [.Alert, .Sound, .Badge], categories: nil)
            UIApplication.sharedApplication().registerUserNotificationSettings(settings)
            UIApplication.sharedApplication().registerForRemoteNotifications()
        }