The Pragmatic Ball boy

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

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()
        }

Xcode8のxcodebuildでarchive時にsignining周りでエラーがでたときの対処法

Xcode8でxcodebuildを使ってコマンドラインでarchiveしようとすると以下のようなエラーがでて失敗するようになりました。

Check dependencies
XXX has conflicting provisioning settings.XXX is automatically signed, but provisioning profile XXX_Adhoc has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor, or switch to manual signing in the project editor.
Code signing is required for product type 'Application' in SDK 'iOS 10.0'

使っていたコマンドは以下のような感じで、通常の設定では実機で試しやすいようにCode Signing IdentityをiOS Developerにしていて、 コマンドラインで実行するときにDistributionに変更するようにしていました。

xcodebuild \
  -workspace XXX \
  -scheme XXX \
  -configuration XXX \
  -archivePath XXX \
  CODE_SIGN_IDENTITY="iPhone Distribution: XXX (XXXX)" \
  archive

がXcode8ではこれでは通りません

対処法

Xcode8からは自動でsigningを管理する機能がついたため、これをxcodebuildで変更しようとするとコンフリクトしてると怒られるみたいです、エラーを見る限り。。

自分のケースだとコマンドラインでのビルド時にCode Signingを変更したいので、自動管理を諦めます。

よって、まずは、Project SettingのGeneralタブのSigningにある"Automatically manage signingをOFFにします

f:id:yanamura:20160916115640p:plain

これだけでは解決せず、Xcode8から追加されたPROVISIONING_PROFILE_SPECIFIERを設定してやると解決しました。 PROVISIONING_PROFILE_SPECIFIERにはprovisioning profileの名前をいれればOKです。

xcodebuild \
  -workspace XXX \
  -scheme XXX \
  -configuration XXX \
  -archivePath XXX \
  CODE_SIGN_IDENTITY="iPhone Distribution: XXX (XXXX)" \
  PROVISIONING_PROFILE_SPECIFIER=XXX\
  archive

Xcode8でテストが実行できない Could not determine bundle identifier for TEST_HOST

Xcode8でxcodebuildでtestを走らせると以下のようなエラーがでてテストが実行できなくなりました

xcodebuild: error: Failed to build workspace XXX with scheme XXX.
    Reason: Could not determine bundle identifier for XXXTests's TEST_HOST: 

どうもTEST_HOST、つまりテスト対象のビルドバイナリがないと怒られてるみたいで、 testの前にbuildを追加したら通りました。

$ xcodebuild -workspace XXX -scheme XXX -configuration Debug clean build test -destination 'platform=iOS Simulator,OS=10.0,name=iPhone 6'

ちなみに、xcodebuildじゃなくて、Xcodeでテスト実行する際も、ビルドする前にいきなりテストを実行すると同じエラーが発生します。 これについては、先にビルドしておけば解決します。

動画のフォトアルバムへの保存

動画のフォトアルバムへの保存

iOS8まで

import AssetsLibrary

...

ALAssetsLibrary().writeVideoAtPathToSavedPhotosAlbum(filePathURL) { _ in
    // 完了後の処理
}

iOS9以降

import Photos

...

PHPhotoLibrary.sharedPhotoLibrary().performChanges(
    {
        PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(filePathURL)
    },
    completionHandler: { _ in
        // 完了後の処理
    }
)

Android Studioをインストールしてプロジェクトをビルドするときに躓いたところ

Androidの既存のプロジェクトをビルドしようとしてAndroid Studioのインストールからはじめると 毎回同じようなところで躓いている気がするのでメモ

エラーで Unsupported major.minor version 52.0 が出た時

原因

これはAndroidStudioが使っているJDKが古いと出る

対策

  1. まずはJDK8をインストールする
  2. AndroidStudioを起動して、File -> Other Settings -> Default Project Structure を選択
  3. JDK locationを変更してJDK8のPathに変更する

エラーで Error:failed to find Build Tools revision 23.0.0 rc3 が出た時

原因

これは対応するAndroid SDK Build Toolsがインストールされていないのが原因

対策

  1. AndroidStudioを起動、Tools -> Android -> SDK ManagerでSDK Managerを起動する。
  2. SDK Toolsのタブを選択し、下の方にある"Show Package Details"のチェックボックスにチェックを入れる
    • f:id:yanamura:20160902193059p:plain
  3. そうするとAndroid SDK Build Toolsの全てのバージョンが表示されるので、必要な物にチェックを入れてインストールする

既存のプロジェクトでSwift2.3のままXcode8でビルドを通す

Xcode8でビルドするとデフォルトではSwift3でコンパイルされてしまいます。 とりあえずXcode8にあげるけどSwift2.3のままにしておいて、Swift3対応は後でという場合の対処方法です。

Build Settings

Build Settingsの"Use Legacy Swift Language Version"を"Yes"にする

  • f:id:yanamura:20160830183604p:plain

Cocoapodsの変更

Podfileの一番最後に以下を追加

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = '2.3'
    end
  end
end

ライブラリをXcode8&Swift2.3対応のブランチを指定するようにする

Swift2.3とSwift3ではバイナリ互換がないので、ライブラリも含めてSwift2.3でコンパイルする必要があります

pod 'Alamofire', :git => 'https://github.com/Alamofire/Alamofire', :branch => 'swift2.3'
pod 'Realm', :git => 'https://github.com/realm/realm-cocoa.git', :branch => 'master', :submodules => true
pod 'RealmSwift', :git => 'https://github.com/realm/realm-cocoa.git', :branch => 'master', :submodules => true