Pebble Coding

ソフトウェアエンジニアによるIT技術、数学の備忘録

Android Studio2.3 から外部エディタvimを開く(macOS)

Android Studio2.3で開いているファイルを行番号を指定し外部エディタvimで開く方法です。 macOSの場合です。

1) Android Studio - Preferences - Tools - External Tools に gvim を登録します。

内部console画面が開かないように Open console のチェックは外します。

f:id:pebble8888:20170325095630p:plain

2) Android Studio - Preferences - Keymap で追加したツール gvim にショートカットキーを割り当てます。 ここでは、Ctrl + Enter に割り当てています。 このショートカットは他のコマンドとコンフリクトしているため、登録時に他のコマンドを破棄するかどうか聞かれます。

f:id:pebble8888:20170325100009p:plain

C# Microsoft純正ライブラリでJSONをdeserializeする

C#JSONをserialize/deserializeする方法として、Microsoftが提供するライブラリを使う場合、2種類の方法がある。

1) DataContractJsonSerializer

Namespace: System.Runtime.Serialization.Json
AssemblySystem.Runtime.Serialization

2) JavaScriptSerializer

Namespace: System.Web.Script.Serialization
Assembly: System.Web.Extensions

1)の方はJSONに対応するモデルクラスを経由して変換する方法。
2)の方は1) の方法に加えて、モデルクラスを経由せずにダイナミックな変数を経由して変換する方法が使える。

この記事ではモデルクラスを作らずにdeselializeする方法を紹介する。

var jsonstring = "{cast:[{id:1, name:\"harry\"},{id:2, name:\"hermione\"}]}";
Debug.Print(jsonstring);
var serializer = new JavaScriptSerializer();
dynamic json = serializer.DeserializeObject(jsonstring);
dynamic cast = json["cast"];
foreach (var obj in cast)
{
    if (obj.ContainsKey("unknowntag"))
    {
        Debug.Assert(false); 
    } else
    {
        Debug.Assert(true);
    }
    if (obj.ContainsKey("name"))
    {
        Debug.Assert(true);
    } else
    {
        Debug.Assert(false);
    }
    int id = obj["id"];
    Debug.Print("{0}", id);
    string s = obj["name"];
    Debug.Print("{0}", s);
}
{cast:[{id:1, name:"harry"},{id:2, name:"hermione"}]}
1
harry
2
hermione

以上である。
注意するのは、型が確定していない部分では、varではなくdynamicで受けるという点である。
dynamicというのはC#で動的に型が決定されるという恐ろしい代物であるが(導入されたのは2010年頃)、
jsonとはすこぶる相性がいい。というか、これがないとJavaScript, Ruby, Swiftと勝負できない。
ContainsKeyの呼び出しによって、keyがふくまれるかどうかも判定することができる。
もちろん、不定な動作をしたときは例外が発生するので、例外のキャプチャは必須であろう。

RxSwift 超入門その3

class GeolocationViewController: ViewController {
    
    @IBOutlet weak private var noGeolocationView: UIView!
    @IBOutlet weak private var button: UIButton!
    @IBOutlet weak private var button2: UIButton!
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // No GeoLocation Viewを追加する
        view.addSubview(noGeolocationView)
        
        let geolocationService = GeolocationService.instance
        
        // GeoLocation が利用できる場合は No GeoLocation Viewを隠す
        geolocationService.authorized
            .drive(noGeolocationView.rx.isHidden)
            .disposed(by: disposeBag)
        
        // ロケーションが変化したら、座標ラベルを更新する
        geolocationService.location
            .drive(label.rx.coordinates)
            .disposed(by: disposeBag)
        
        // No GeoLocation Viewにあるプリファレンスオープンボタン
        button.rx.tap
            .bindNext { [weak self] in
                self?.openAppPreferences()
            }
            .disposed(by: disposeBag)
        
        // GeoLocation Viewにあるプリファレンスオープンボタン
        button2.rx.tap
            .bindNext { [weak self] in
                self?.openAppPreferences()
            }
            .disposed(by: disposeBag)
    }
    
    private func openAppPreferences() {
        UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!)
    }
}

RxSwift 超入門その2

let minimalUsernameLength = 5
let minimalPasswordLength = 5

class SimpleValidationViewController : ViewController {

    @IBOutlet weak var usernameOutlet: UITextField!
    @IBOutlet weak var usernameValidOutlet: UILabel!

    @IBOutlet weak var passwordOutlet: UITextField!
    @IBOutlet weak var passwordValidOutlet: UILabel!

    @IBOutlet weak var doSomethingOutlet: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 初期状態で表示するワーニングメッセージ
        usernameValidOutlet.text = "Username has to be at least \(minimalUsernameLength) characters"
        // 初期状態で表示するワーニングメッセージ
        passwordValidOutlet.text = "Password has to be at least \(minimalPasswordLength) characters"

        // ユーザー名が有効かどうか
        let usernameValid = usernameOutlet.rx.text.orEmpty
            .map { $0.characters.count >= minimalUsernameLength }
            .shareReplay(1)

        // パスワードが有効かどうか
        let passwordValid = passwordOutlet.rx.text.orEmpty
            .map { $0.characters.count >= minimalPasswordLength }
            .shareReplay(1)

        // ユーザー名とパスワードがともに有効かどうか
        let everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
            .shareReplay(1)

        // ユーザー名が有効になったらパスワード入力欄をenabledにする。その逆も行う。
        usernameValid
            .bindTo(passwordOutlet.rx.isEnabled)
            .disposed(by: disposeBag)

        // ユーザー名が有効になったらユーザー名不備メッセージを隠す。その逆も行う。
        usernameValid
            .bindTo(usernameValidOutlet.rx.isHidden)
            .disposed(by: disposeBag)

        // パスワードが有効になったらパスワード不備メッセージを隠す。その逆も行う。
        passwordValid
            .bindTo(passwordValidOutlet.rx.isHidden)
            .disposed(by: disposeBag)

        // ユーザー名、パスワードがともに有効になったらボタンを有効にする。その逆も行う。
        everythingValid
            .bindTo(doSomethingOutlet.rx.isEnabled)
            .disposed(by: disposeBag)

        // ボタンがタップされた時の処理
        doSomethingOutlet.rx.tap
            .subscribe(onNext: { [weak self] in self?.showAlert() })
            .disposed(by: disposeBag)
    }

    func showAlert() {
        let alertView = UIAlertView(
            title: "RxExample",
            message: "This is wonderful",
            delegate: nil,
            cancelButtonTitle: "OK"
        )

        alertView.show()
    }

}

このサンプルから学習できることは、.sharePlay(1)です。
これは無駄に同じ処理が複数回走るのを防ぐためのメソッドと考えて良いです。

RxSwift 超入門その1

0)環境: Xcode8.2.1

1) 新規iOSプロジェクトaaaを作成する。

2) aaaフォルダに新規ファイルPackage.swiftを以下の内容で作成する。

import PackageDescription

let package = Package(
    name: "dummy",
    targets: [], 
    dependencies: [
        .Package(url: "https://github.com/ReactiveX/RxSwift.git", majorVersion: 3)
    ]
)

3) aaaフォルダで

$ swift build

を実行する。エラーが出るが気にしない。

4) aaa.xcodeprojに、Packages/RxSwift-3.2.0/Rx.xcodeprojを追加し、
aaa.xcodeprojのターゲットaaaの
Target Dependencies
RxCocoa-iOS,RxSwift-iOSの2つを追加する。
Link Binary With Libraries
にもRxCocoa-iOS,RxSwift-iOSを追加し、ビルドする。

5) aaa.xcodeprojのMain.storyboardにUITextLabelを2つとUILabel一つを追加する。

6) ViewControllerの先頭に以下のソースを追加する。

import RxSwift
import RxCocoa

7) ViewControllerに以下のソースを追加し3つの@IBOutletを接続して実行する。

import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
    @IBOutlet weak var field1:UITextField!
    @IBOutlet weak var field2:UITextField!
    @IBOutlet weak var label:UILabel!
    var disposeBag = DisposeBag()
    override func viewDidLoad() {
        super.viewDidLoad()
        Observable.combineLatest(field1.rx.text.orEmpty,
                                 field2.rx.text.orEmpty) {
                                    textValue1, textValue2 -> Int in
                return (Int(textValue1) ?? 0) + (Int(textValue2) ?? 0)
            }
            .map { $0.description }
            .bindTo(label.rx.text)
            .disposed(by: disposeBag)
    }
}

解説:
.rxはUITextFieldやUILabelなどUIKitのコントロールに追加されたswift extensionです。
combineLatestは2つの引数を持ちますが、2つの引数のどちらかに変化が発生した時に、イベントを発生させます。
その時、2つの引数の最新の値を利用してクロージャが実行され、以降の.mapの処理へと続きます。
このフローは、このViewControllerが破棄された時に不要となるため、このViewControllerのメンバ変数であるdisposeBagを使って.disposed(by:)することによって、ViewControllerの破棄タイミングでこのフローも破棄されるようにしています。