vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】【Unity】DictionaryとListの速度比較

はじめに

ここ最近 Unity 2017 Game Optimization を読んでいるのですが、
その中の一つに格納したデータを検索したりするなら List を使うより Dictionary を使うと良いよ、というものがありました。

これはどんな検索の仕方でも早くなるの?など気になったので調べてみました。

サンプルデータ

Dictionary は Key と Value を1対で持ちます。

List でも同じようにデータを対で持たせるため、下記のようなクラスを使うことにします。

SampleValue.cs

public class SampleValue {
    public int Id { get; set; }
    public string Category { get; set; }
}

で、データは下記のようにセットします(検証内容によって途中で変えますが)。

DictionarySample.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UI;
using Debug = UnityEngine.Debug;

public class DictionarySample: MonoBehaviour {
    public Button ListButton;
    public Button DictionaryButton;        
    private List< SampleValue> sampleList;
    private Dictionary< int, string> sampleDictionary;
        
    private void Start() {
        sampleList = new List< SampleValue>();
        sampleDictionary = new Dictionary< int, string>();

        var categoryNum = 0;
        for (var i = 0; i < 100000; i++) {
            var newValue = new SampleValue {
                Id = i,
                Category = "Category" + categoryNum
            };
            sampleList.Add(newValue);
            sampleDictionary.Add(i, "Category" + categoryNum);

            categoryNum++;
            if (categoryNum > 5) {
                categoryNum = 0;
            }
        }
        ListButton.onClick.AddListener(() => {
            Profiler.BeginSample("SamplingProfile");
           
            // 検証用の処理呼び出し.
            OutputListValues();
                
            Profiler.EndSample();
        });
        DictionaryButton.onClick.AddListener(() => {
            Profiler.BeginSample("SamplingProfile");

            // 検証用の処理呼び出し.
            OutputDictionaryValues()

            Profiler.EndSample();
        });
    }        
}

計測1. 格納したデータをそのまま取り出す

まずは格納したデータを、検索などせずにそのまま取り出してみます。

DictionarySample.cs

~省略~
private void OutputListValues() {
    foreach (var s in sampleList) {
        result = s.Category;
    }
}
private void OutputDictionaryValues() {
    foreach (var s in sampleDictionary) {
        result = s.Value;
    }
}
~省略~

f:id:mslGt:20180118003425j:plain

List の方が良い結果となりました。

計測2. 指定したKeyに合致するデータを取り出す

Key (List では Id) の値が合致するデータを取り出してみます。
今回 Id と List の要素番号が同じであるためあまり意味がない部分もありますが、そこは気にしない方向で。

なお Dictionary と List の速度比較の話を検索すると、この方法で検証していることが多いです。

DictionarySample.cs

~省略~
private void OutputListValues() {
    var result = "";
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Id == i) {
            result = sampleList[i].Category;
        }
    }
}
private void OutputDictionaryValues() {
    var result = "";
    for (var i = 0; i < sampleDictionary.Count; i++) {
        result = sampleDictionary[i];
    }
}
~省略~

う~ん?一回目は List の方が速い、という結果になりました(誤差の範囲内かもですが)。

f:id:mslGt:20180118003517j:plain

計測3. 指定したKeyに合致するデータを取り出す(Listは要素番号指定)

今回 List が持つ Id と、要素番号は全く同じです。

このようなデータの場合に、 List の要素番号を指定してデータを取り出すのと Dictionary で Key (Id, 要素番号と同じ) を指定してデータを取り出すのとではどちらが速いでしょうか。

DictionarySample.cs

~省略~
private void OutputListValues() {
    for (var i = 0; i < sampleList.Count; i++) {
        result = sampleList[i].Category;
    }
}

// Dictionaryは上記と同じ.

~省略~

f:id:mslGt:20180118003658j:plain

そもそも List での Id 比較の速度による影響が少なかったようで、
あまり変わりませんでした。

計測4. Valueを検索する

では、 Dictionary の Key ではなく Value から要素を取得する場合はどうでしょうか。

「Category0」という値を持った要素を取得してみることにします。

DictionarySample.cs

~省略~
private void OutputListValues() {
    var result = "";
    foreach (var category in sampleList.Where(c => c.Category == "Category0")) {
        result = category.Category;
    }
}

private void OutputDictionaryValues() {
    var result = "";
    foreach (var category in sampleDictionary.Where(c => c.Value == "Category0")) {
        result = category.Value;
    }
}
~省略~

f:id:mslGt:20180118003817j:plain

GC Alloc は List の方が少なく、 速度は Dictionary の方が速い、という結果になりました。

計測5. Keyをstringにしてみる

今のところ List で良くね?て結果ばかりが出て悲しいので、
もう少し Dictionary の得意分野とされる、 Key によるアクセスをもう少し見てみることにします。

まずは今まで int にしていた Key の型を string にしてみます。

SampleValue.cs

public class SampleValue {
    public string Id { get; set; }
    public string Category { get; set; }
}

DictionarySample.cs

~省略~
private List< SampleValue> sampleList;
private Dictionary< string, string> sampleDictionary;
    
private void Start() {
    sampleList = new List< SampleValue>();
    sampleDictionary = new Dictionary< string, string>();

    var categoryNum = 0;
    for (var i = 0; i < 100000; i++) {
        var newValue = new SampleValue {
            Id = "Category" + i,
            Category = "Category" + categoryNum
        };
        sampleList.Add(newValue);
        sampleDictionary.Add("Category" + i, "Category" + categoryNum);
        categoryNum++;
        if (categoryNum > 5) {
            categoryNum = 0;
        }
    }
    ListButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");
        OutputListValues();
        
        Profiler.EndSample();
    });
    DictionaryButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");

        OutputDictionaryValues();
        
        Profiler.EndSample();
    });
}
private void OutputListValues() {
    var result = "";
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Id == "Category10") {
            result = sampleList[i].Category;
        }
    }
}
private void OutputDictionaryValues() {
    var result = "";
    result = sampleDictionary["Category10"];
}

で、結果はこちら。

f:id:mslGt:20180118004107j:plain

Dictionary の本領発揮、という感じですね。
(ただ、一回目の直前にスパイクが見られるのが気になりますが)

f:id:mslGt:20180118004135p:plain

やはりループしなくても値が取れる、というのは大きい。

計測6. KeyをCustom classにしてみる

次は Key として Custom class を指定してみます。

List でも Value として string の値を持てるよう、下記のクラスを値として持たせるようにします。

SampleValueForList.cs

public class SampleValueForList {
    public SampleValue Key { get; set; }
    public string Value { get; set; }
}

DictionarySample.cs

~省略~
private List< SampleValueForList> sampleList;
private Dictionary< SampleValue, string> sampleDictionary;

private void Start() {
    sampleList = new List< SampleValueForList>();
    sampleDictionary = new Dictionary< SampleValue, string>();

    var categoryNum = 0;
    for (var i = 0; i < 100000; i++) {
        var newValue = new SampleValue(i, "Category" + categoryNum);
        sampleList.Add(new SampleValueForList {
            Key = newValue,
            Value = "Category" + categoryNum
        });
        sampleDictionary.Add(newValue, "Category" + categoryNum);
            
        categoryNum++;
        if (categoryNum > 5) {
            categoryNum = 0;
        }
    }
    ListButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");
        OutputListValues();
        
        Profiler.EndSample();
    });
    DictionaryButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");

        OutputDictionaryValues();
        
        Profiler.EndSample();
    });
}
private void OutputListValues() {
    var result = "";
    var key = new SampleValue(0, "Category0");
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Key.Equals(key)) {
            result = sampleList[i].Value;
            Debug.Log("result " + result);
        }
    }
}
private void OutputDictionaryValues() {
    var result = "";
    var key = new SampleValue(0, "Category0");
    if (sampleDictionary.TryGetValue(key, out result)) {
        Debug.Log("result " + result);
    }
}

ただしそのままだと Dictionary 、 List ともに結果は 0 件になります。

Dictionary で指定した Key が存在するかを確認するのは FindEntry というメソッドですが、
この中でも登場する GetHashCode 、 Equals を override して Key として渡された SampleValue と比較できるようにする必要があります。

Dictionary

~省略~
private int FindEntry(TKey key) {
    if ((object) key == null)
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    if (this.buckets != null) {
        int num = this.comparer.GetHashCode(key) & int.MaxValue;
        for (int index = this.buckets[num % this.buckets.Length]; index >= 0; index = this.entries[index].next) {
            if (this.entries[index].hashCode == num && this.comparer.Equals(this.entries[index].key, key))
                return index;
        }
    }
    return -1;
}
~省略~

SampleValue.cs

public class SampleValue {
    public int Id { get; private set; }
    public string Category { get; private set; }

    public SampleValue(int newId, string newCateogry) {
        Id = newId;
        Category = newCateogry;
    }
    
    public override int GetHashCode() {
        return Id;
    }
    public override bool Equals(object obj) {
        return Equals(obj as SampleValue);
    }
    public bool Equals(SampleValue obj) {
        return obj != null && obj.Id == this.Id && obj.Category == this.Category;
    }
    
}

f:id:mslGt:20180118004242j:plain

Log 出力してみるとわかりますが、 Dictionary を検索するとき GetHashCode で HashCode の値を比較 -> 同じ場合は Equals で比較、
という順番で比較が行われます。
(※実行時間の問題から、要素数は減らしてから確認した方が良いと思います)

また、 Dictionary に値を追加するときにも上記の方法で既存の値を確認していることがわかります。

GetHashCode で得られる値は、すべて異なるものである必要はありません。

ただし、異なる値に同じ HashCode 値が設定されてしまうと、 Equals で確認する量が増え、遅くなります。

試しにすべて 0 を設定してみたところ、 Dictionary に値を追加するところで固まってしまい、
時間の計測ができませんでした。

ということで適切な値を設定する必要があります。

計測7. KeyをCustom classにしてみる(ListでGetHashCode -> Equalsの順に検索)

List でも Dictionary で Key を探すように、 GetHashCode -> Equals の順に検索すると同じように速くなるでしょうか。

DictionarySample.cs

~省略~
private void OutputListValues() {
    var result = "";
    var key = new SampleValue(0, "Category0");
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Key.GetHashCode() == key.GetHashCode() &&
            sampleList[i].Key.Equals(key)) {
            result = sampleList[i].Value;
            Debug.Log("result " + result);
        }
    }
}

// Dictionaryは上記と同じ.

~省略~

f:id:mslGt:20180118004429j:plain

Dictionary には及ばないものの、速くなりましたね。

おわりに

今回の結果を元に考えると、Key を指定することができ、かつ期待する検索結果が少ない場合は Dictionary を使うことで高速化が望めそうです。
反対に検索結果が多い、また色んな条件で検索したい場合は List の方が強い、またはあまり変わらない、ということになりそうです。

また今回言及はしませんでしたが、 Dictionary では List や配列のように順序を持たないため、
要素番号で指定したい、また格納されたデータを決まった順番で扱いたい場合は List を使う必要があります。

SortedDictionary という自動で要素が並び替えられるものもありますが、
パフォーマンス面では Dictionary に劣り、かつ要素番号での指定はできません。

Unity 2017 Game Optimization でも指摘されていますが、
それぞれの特性を活かして効果的に使い分けていきたいところです。

参照

Dictionary

GetHashCode

【C#】自作クラスのグループ化と並び替え

はじめに

2017年に収まらなかったので新年一発目の記事となります。

今年もふと気になったあれこれ、やってみたあれこれを雑多に書き残していきますのでよろしくお願いいたします。

さてさて今回のお題ですが、自作クラスの並べ替えを雑にやろうとしたところ、
全然入れ替わらなかったりうまくいかなかったので、
もう少しちゃんと見てみるかぁ、という内容です。

  • 文字列の List を並び替える
  • 文字列 > int の2つの要素で並び替える
  • 文字列をグループ化した後 int で並び替える

文字列のListを並び替える

まずは文字列の Sort について見てみます。

例えばこのような List があった場合。

var sampleList = new List< string> {
    "カテゴリー1",
    "カテゴリー2",
    "カテゴリー",
    "カテゴリー1",
    "カテゴリー2",
    "カテゴリー5",
};

これを下記のように Sort() を引数なしで実行すると、 List が昇順に並び替えられます。

sampleList.Sort();

これが何をしているのかを見たかったのですが、
List クラスが IComparer< T> を null にして Array.Sort< T> を呼んでいる辺りで処理を見失ってしまいました。

というわけで、それはいったん置いておいて msdn のサンプルを見てみることにします。

Sort メソッドに ICompare を継承し、 int を返す Compare メソッドを渡すことで、
並び替える方法がカスタマイズできます。

サンプルでは比較する2つの値が Null かどうかなども確認していますが、
両方 Null ではない場合の比較は下記の 2 種類です。

int retval = x.Length.CompareTo(y.Length);

if (retval != 0) {
    return retval;
}
else {
    return string.Compare(x, y, StringComparison.Ordinal);
}
  • x は第一引数の文字列、 y は第二引数の文字列です。

上は簡単で、文字数が異なる場合にその差分を返しています。
下は unsafe メソッドを使って、先頭から一文字ずつ比較をしていました。

パフォーマンス的な理由から、文字数などで違いが判別できる場合はそちらを使っているようです。

Compareメソッドの戻り値

Compare メソッドが返す値は大きく分けて 3 種類です。

  • 0 未満: x が y より前に設定される
  • 0: 順序変わらず
  • 0 より大きい: x が y より後に設定される

後で少し触れますが、例えば -1 を返したときと -5 を返したときで、
Sort() で比較対象として引数に渡される回数が変わっていました。

Sort() は何回実行されるのか

最大で O(n ^ 2) (要素数の 2 乗 * 係数)とのことですが、上記のコードの場合は要素数 6 で 20 回実行されました。

自作クラスのListを並び替える

では、下記のような自作クラスを並び替えてみます。

SampleValue.cs

public class SampleValue {
    public int Id { get; set; }
    public string Category { get; set; }
}

で、これを Category > Id の順に並び替えてみます。

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class SortSample : MonoBehaviour {
    private void Start () {
            var sampleList = new List< SampleValue>{
                    new SampleValue {
                        Id = 0,
                        Category = "カテゴリー1",  
                    },
                    new SampleValue {
                        Id = 3,
                        Category = "カテゴリー2",
                    },
                    new SampleValue {
                        Id = 5,
                        Category = "カテゴリー",
                },
                    new SampleValue {
                        Id = 1,
                        Category = "カテゴリー1",
                    },
                    new SampleValue {
                        Id = 2,
                        Category = "カテゴリー2",
                    },
                    new SampleValue {
                        Id = 4,
                        Category = "カテゴリー5",
                    },
        };  
        var compare = new CompareSample();
            // Listを並び替える.
        sampleList.Sort(compare.Compare);

        sampleList.ForEach(v => Debug.Log("ID: " + v.Id + " C: " + v.Category));
    }
}
// 並び替えるためのクラス.
public class CompareSample : IComparer< SampleValue> {
    public int Compare(SampleValue x, SampleValue y) {
        int retval = x.Category.Length.CompareTo(y.Category.Length);
        
        if (retval != 0) {
            return retval;
        }
        else {
            int compareCategory = string.Compare(x.Category, y.Category, StringComparison.Ordinal);
                    // Categoryの値を比較して同じであった場合にIdで並び替え.
            return compareCategory == 0 ? x.Id.CompareTo(y.Id) : compareCategory;
        }
    }
}
  • Sort() は List の中の順番を入れ替えるため戻り値の取得などは行いません。
  • まず Category 同士を比較し、同じ Category であった場合に Id で並び替えています。

実行結果です。

  1. Id: 5 Category: カテゴリー
  2. Id: 0 Category: カテゴリー1
  3. Id: 1 Category: カテゴリー1
  4. Id: 2 Category: カテゴリー2
  5. Id: 3 Category: カテゴリー2
  6. Id: 4 Category: カテゴリー5

どんな順番で実行されるのか

上で実行回数について触れましたが、どんな組み合わせで実行されるのでしょうか。

実際のところは Sort のアルゴリズムを学ばないと理解できないところではありますが、
とりあえず今回のサンプルを動作させた結果を載せておきます。

f:id:mslGt:20180103083245j:plain

List の一番最初に来る「カテゴリー」は 11 回呼ばれているのに対し、
最後に来る「カテゴリー5」は 3 回のみ、かつ第一引数( x )としては一度も呼ばれていませんでした。

この結果だけで考えると、文字列の List を並び替える場合、
それぞれの違いが小さければ小さいほど実行回数は増えるのでは?という感じがしました。

自作クラスのListをグループ化して並び替える

上記では、 Category という文字列に対しても並び替えられていました。

それでは、Category は並び替えず、
同じ Category を持つ Id で並び替えるにはどのようにすれば良いでしょうか。

グループ化する

今回は Linq の GroupBy を使ってみました。

何も考えずに書こうとすると、下記のように書きたくなるのですが、
これだとエラーになります。

// 戻り値が  IEnumerable< IGrouping< string, SampleValue>> であるためエラー.
sampleList.GroupBy(v => v.Category)
    .OrderBy(v => v.Id);

sampleList.GroupBy(v => v.Category) から、 List< SampleValue> の値を取得するには、
SelectMany を使う必要があります。

var sortedList = sampleList.GroupBy(v => v.Category)
    .SelectMany(v => v);

v => v のところが少し変な感じもしますね。

なお、 foreach を使って取得することもできます。

var sortedList = sampleList.GroupBy(v => v.Category);

foreach(var k in sortedList) {
    foreach (var v in k) {
        Debug.Log(v.Category + " Id " + v.Id);
    }
}

結果は下記の通りです。

  1. Id: 0 Category: カテゴリー1
  2. Id: 1 Category: カテゴリー1
  3. Id: 3 Category: カテゴリー2
  4. Id: 2 Category: カテゴリー2
  5. Id: 5 Category: カテゴリー
  6. Id: 4 Category: カテゴリー5

グループ化した List を並び替える

あとはこれをグループごとに並び替えます。

といってこのような感じで並び替えてしまうと、
グループ化が解除されて Id のみで並び替えられてしまいます。

var sortedList = sampleList.GroupBy(v => v.Category)
            .SelectMany(v => v)
            .OrderBy(v => v.Id);

※今回のデータの作りがまずく、このまま実行すると一見そろっているように見えますが、
Category の値を変えるとグループ化されていないのがわかります。

これを防ぐためには、 SelectMany の中で並び順を指定する必要があります。

var sortedList = sampleList.GroupBy(v => v.Category)
            .SelectMany(v => v.OrderBy(x => x.Id));

結果は下記の通りです。

  1. Id: 0 Category: カテゴリー1
  2. Id: 1 Category: カテゴリー1
  3. Id: 2 Category: カテゴリー2
  4. Id: 3 Category: カテゴリー2
  5. Id: 5 Category: カテゴリー
  6. Id: 4 Category: カテゴリー5

SelectManyについて

では今回活躍してくれた、 SelectMany についてもう少し見てみることにします。

SelectMany を使うことで、 IEnumerable< IGrouping< string, SampleValue>> という型で戻ってくる GroupBy の結果を、
IEnumerable< SampleValue> という型に変換できます。

で、この SelectMany のソースをたどってみると、先ほど値を取り出したときと同じように、
foreach を使って値を取り出していました。

Enumerable.cs

private static IEnumerable< TResult> SelectManyIterator< TSource, TResult>(IEnumerable< TSource> source, Func< TSource, IEnumerable< TResult>> selector) {
    foreach (TSource source1 in source) {
        foreach (TResult result in selector(source1))
            yield return result;
    }
}

そのため、下記のようにすれば SelectMany を使わず同様のことができます。
(需要があるかはわかりませんが)

private void Start() {
    ~省略~

    var sortedList = GetSortedSampleValues(sampleList.GroupBy(v => v.Category));
}
private IEnumerable< SampleValue> GetSortedSampleValues(IEnumerable< IGrouping< string, SampleValue>> groupedValue) {
    foreach(var k in groupedValue) {
        // IGrouping< string, SampleValue> を IdでSort.
        foreach (var v in k.OrderBy(v => v.Id)) {
            yield return v;
        }
    }
}

一応両者を計測してみたところ、若干 Foreach の方が良い結果になっていました。

f:id:mslGt:20180103083531j:plain

おわりに

Linq は良いぞ

…まぁ使用状況によっては for を使う場合に比べて GC alloc の量が増えたり、
いつでもどこでも使おうぜ!とまでは言い難いです。

ただ、今回のように複雑な処理がわずか数行で書けてしまうのはすごいですね。

参照

Sort

GroupBy

SelectMany

Others

【Xamarin.iOS】Xibで作ったTableViewCellを動的に追加したい

はじめに

以前書きましたが、 iPhone X 買いました。

マスクしてると顔認証してくれないとか不満が完全に無いわけではありませんが、
まぁ良好、といったところです。

で、せっかくなので何かアプリを作りたいですよね?

ということで、まずは目覚ましアプリに挑戦してみることにしました。

環境と仕様

開発言語

言語ですが、通常なら Swift となるわけですが、今回は Xamarin.iOS を使ってみることにしました。

ここ最近 Xamarin.Forms を使っていて Xamarin.iOS にも興味があった、ということが大きな理由です。

また、お仕事でもごくまれに Swift を使ってアプリを作る時があるのですが (3D を使わず、かつ BLE などネイティブの機能が必要な場合) 、
ほとんど触ったことのない Swift を言語仕様を調べながら書くほうが良いのか、
曲がりなりにも日常的に触れている C# を使って、 Xamarin.iOS の仕様を調べながら書くほうが良いのかは調べてみたいと思っていました。

なおXamarin.Forms を選ばなかった理由は、単に Xamarin.iOS を使ってみたかったからですw

Swift は Swift で注目すべき言語の一つだとは思いますので、別途追いかけたいところです。

開発環境

WindowsMacVisual Studio を使って開発しています。

本当は Windows からリモート接続で開発をしたいところなのですが、
ネットワーク状況からかなかなか接続できない時があったりするため、
Mac と接続していないとできない Storyboard 操作の部分は Mac でやっちゃうようにしています。

Mac だけにしない理由は家の Mac がラップトップでない (Mac mini) ことと、
2012 年モデルなのでさすがに動きがちょっともっさりしているというのが理由です。

仕様

まずはできるだけシンプルに作ることにします。

  • セットした時間になったら目覚ましが鳴る
  • 目覚ましが鳴る時間を追加、編集、削除できる
  • 目覚ましを曜日で指定できる
  • 目覚ましの On/Off ができる

とりあえず日本時間のみを考慮し、サマータイムなどは考えないことにします。
(うるう年とかは考慮が必要)

あと、次の 24 時間だけ目覚ましをキャンセルする機能も追加したいと思います。

Android 標準の目覚ましではこの機能があり、便利に使っていたためです。
(朝にあんまり強くないので、目覚ましを複数回設定するので)

現状、とりあえずの見た目はこんな感じです。

f:id:mslGt:20171230074435p:plain

親となる View にヘッダーと UITableView を設置し、
UITableView に UITableViewCell を追加しています。

StoryboardとViewController

Swift や Objective-C でアプリを作る場合、
Storyboard にボタンなどを追加し、 Outline で ViewController に紐づけます。

Xamarin.iOS も同じかな?と思ったのですが、実は Xamarin.Forms の XAML とコードビハインドの関係と同じように、
Storyboard で Identity の Name に名前を設定しておくと、
ViewController からその名前でアクセスすることができます。

すごい!

コードでボタンを追加する

追加ボタンを押したときに、動的に要素を追加したい。

ということで、まずは試しに ViewController からボタンを追加してみます。

ViewController.cs

using System;
using UIKit;

namespace XamarinIOsSample
{
    public partial class ViewController : UIViewController
    {
        private UIButton button;
        
        public ViewController(IntPtr handle) : base(handle)
        {
            // 親となるViewに対する設定.
            View.BackgroundColor = UIColor.White;
            Title = "My Custom View Controller";
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            
            // UITableViewの背景色をセット.
            MainView.BackgroundColor = UIColor.Red;

            // ボタンを作成して TableView に追加する.
            button = UIButton.FromType(UIButtonType.System);
            button.Frame = new CoreGraphics.CGRect(20, 20, 280, 44);
            button.SetTitle("Click Me", UIControlState.Normal);
            MainView.AddSubview(button);

            button.TouchUpInside += (sender, e) => {
                // ボタン押下時の処理.
            };
        }
        public override void DidReceiveMemoryWarning()
        {
            base.DidReceiveMemoryWarning();
            // Release any cached data, images, etc that aren't in use.
        }
    }
}

UITableView への背景色の設定やボタンの追加は ViewDidLoad() で行っています。

これは、コンストラクターで実行してしまうとそのあと上書きされてしまい、
設定した内容が反映されない(ように見える)ためです。

なお公式ページにはコードだけで Window から追加していく方法が紹介されています。
この充実ぶりもすごいです。

コードでXibを追加する

さてそれでは実際にアイテムを追加できるようにしてみます。

ただ、先ほどのようにボタンやラベルを一つずつ追加して位置を指定していくのは、
コード量も多くなるしあまりに大変。

Unity の Prefab のように、一緒に扱う UI をグループ化して取り扱う方法として、
Xib を使う、というものがあります。
(TableView にセットする UITableViewCell の場合)

と、ここまでは良かったのですが。

Xibを作成する

追加 > クラス から UITableViewCell のテンプレートを選択してファイル作成すると、
Xib ファイルとコードビハインドが生成されます。

この時「Cell」という名前にするのが良さそうです。(理由は後述)

とりあえず下記のようにアイテムを並べて、それぞれ名前を付けておきます。

f:id:mslGt:20171230074519p:plain

で、これを UITableView に追加してみます。

UITableViewSource

UITableView を使う場合、そのデータのソースとなる UITableViewDataSource と、
ふるまいを指定する UITableViewDelegate を継承したクラスを作成し、セットする必要があります。

Xamarin.iOS における UITableViewSource は、 UITableViewDataSource と UITableViewDelegate を1つのクラスにまとめたもの、
ということなので、これを継承したクラスを作ってあげれば OK です。

C# ではクラスの多重継承ができない、という理由もあるのでしょうが、
ただ Swift や Objective-C の代わりに C# で書ける、
という以外にも細かい部分で改善されている部分があるのが良いですね。

DataSource.cs

using System;
using Foundation;
using UIKit;

using System.Collections.Generic;
using XamarinIOsSample.Value;

namespace XamarinIOsSample
{
    public class DataSource : UITableViewSource
    {
        private readonly List< AlarmItem> objects = new List< AlarmItem>();
        
        public IList< AlarmItem> Objects => objects;

        public override nint RowsInSection(UITableView tableview, nint section)
        {
            return objects.Count;
        }
        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            // UITableViewCellを作成したXibのクラスとして取得する.
            if (!(tableView.DequeueReusableCell(Cell.Key) is Cell cell))
            {
                return null;
            }
            // ラベルやボタンに対する設定を行う.
            cell.TimeLabelText = objects[indexPath.Row].TimeSetText;
            cell.DayOfTheWeekLabelText = objects[indexPath.Row].DayOfTheWeekText;
            cell.OnOffSwitchStatus = objects[indexPath.Row].OnOffSwitchStatus;

            return cell;
        }

        public override bool CanEditRow(UITableView tableView, NSIndexPath indexPath)
        {
            // Return false if you do not want the specified item to be editable.
            return true;
        }
    }
}

とりあえず必要そうなメソッドだけで作成してみました。

ラベルのテキストなどに設定する値を格納するクラスの変数をリスト化して Cell の数ぶん保持しています。

AlarmItem.cs

namespace XamarinIOsSample.Value
{
    public class AlarmItem
    {
        public string TimeSetText { get; set; }
        public string DayOfTheWeekText { get; set; }
        public bool OnOffSwitchStatus { get; set; }
    }
}

あとは Xib のコードビハインドでこれらの値をセットできるようにします。

Cell.cs

using System;

using Foundation;
using UIKit;

namespace XamarinIOsSample
{
    public partial class Cell : UITableViewCell
    {
        public static readonly NSString Key = new NSString("Cell");
        public static readonly UINib Nib;

        public string TimeLabelText { 
            get => TimeLabel.Text;
            set => TimeLabel.Text = value;
        }

        public string DayOfTheWeekLabelText
        {
            get => DayOfTheWeekLabel.Text;
            set => DayOfTheWeekLabel.Text = value;
        }
        public bool OnOffSwitchStatus
        {
            get => OnOffSwitch.On;
            set => OnOffSwitch.On = value;
        }

        static Cell()
        {
            Nib = UINib.FromName("Cell", NSBundle.MainBundle);
        }

        protected Cell(IntPtr handle) : base(handle)
        {
            // Note: this .ctor should not contain any initialization logic.
        }
    }
}

直接ラベルなどを返すようにした方が良いのかな?とも思いますが、
まず今回はこのような形にしました。

最後に ViewController から UITableView への追加や Table 行の高さなどを設定してみます。

ViewController.cs

using System;
using UIKit;
using XamarinIOsSample.Value;

namespace XamarinIOsSample
{
    public partial class ViewController : UIViewController
    {
        private DataSource dataSource;
~省略~
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            // Xibの読み込み.
            MainView.RegisterNibForCellReuse(Cell.Nib, Cell.Key);
            MainView.Source = dataSource = new DataSource();
            
            // UITableView.AutomaticDimensionだと正しく設定されなかったのでいったんベタ打ち.
            MainView.RowHeight = 124;

            var newItem = new AlarmItem();
            newItem.TimeSetText = "06:30";

            dataSource.Objects.Insert(0, newItem);

            var newItem2 = new AlarmItem();
            newItem2.TimeSetText = "07:00";

            dataSource.Objects.Insert(1, newItem2);

        }
~省略~
    }
}

NSUnknownKeyException

上記で UITableViewCell を作るときに、名前を「Cell」にした理由です。

なぜか別の名前で作成してしまうと、
ViewController や DataSource での名前を変更しても NSUnknownKeyException が発生しました。

どこかに「Cell」という名前で紐づけがあるのだと思うのですが、
それがどこかはわからず。。。

これについては解決しましたら追記か別途書くようにしたいと思います。

おわりに

Storyboard や Xib など iOS アプリ開発に関する知識や Xamarin.iOS についての知識はともかく、
日常的に使い慣れた言語で書けるというのは良いですね。

もう少し作りこんだら、 Swift で書くとどうなるの?というのも試してみたいと思います。

参照

【C#】Listを検索してList(またはIEnumerable)を返したい

はじめに

この記事は C# Advent Calendar 2017 の21日目の記事です。

qiita.com

すでに何度かネタにしている Effective C# から。

第四章の29項目で、 Collection を作って返すよりも Iterator メソッドを返した方が良い、というものがあります。

理由としては遅延評価であることや、呼び出す側で都合が良いように加工しやすい、といったところがあるようです。

ただそこで気になったのがパフォーマンス。

例えばデータを保持するクラスの List があったとして、
その中の1要素だけを取ってきて for や foreach で処理したい。

この時、下記の条件で実行すると実行速度や GC Alloc のデータ量にどの程度違いがあるのでしょうか。

  1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す
  2. IEnumerable を使って Iterator メソッドとして返す
  3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用)
  4. 対象の要素だけを持った List をメソッド内で生成し、それを返す
  5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用)

  6. 1.について、 List を生成する時間は計測せず、 List を返すメソッドの実行時間のみを計測します。

感覚的には番号順通りに速い、という気がしますがどうでしょうか。

実行環境

  • Windows 10 Pro 64bit
  • Unity 2017.2.0f3

Unity を使うのは、計測に Profiler を使いたいのと、
重いデータとして 3D モデルを持たせるようにしたいためです。

データを格納するクラス

名前は特に気にしない方向で...。

Breadman.cs

using System.Collections.Generic;
using UnityEngine;

public class Breadman{
    public int Id { get; set; }
    public GameObject BreadmanModel { get; set; }
    public List SpecialMoves { get; set; }
}

こんな感じでデータを入れておきます。
また検証 1. のための List も作成します。

MainController.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class MainController : MonoBehaviour {
    public GameObject BreadmanModel1;
    public GameObject BreadmanModel2;
    public GameObject BreadmanModel3;
    ~省略~
    private List breadmen;
    private List breadmanNames;
    
    private void Start () {

        breadmen = new List {
            new Breadman {
                Id = 0,
                BreadmanModel = BreadmanModel1,
                SpecialMoves = new List {
                    "パンチ",
                    "キック",
                }
            },
            new Breadman {
                Id = 1,
                BreadmanModel = BreadmanModel2,
                SpecialMoves = new List {
                    "空を飛ぶ",
                }
            },
            new Breadman {
                Id = 2,
                BreadmanModel = BreadmanModel3,
                SpecialMoves = new List {
                    "水に濡れる",
                }
            },
            new Breadman {
                Id = 3,
                BreadmanModel = BreadmanModel4,
                SpecialMoves = new List {
                    "力が出ない",
                }
            },
            new Breadman {
                Id = 4,
                BreadmanModel = BreadmanModel5,
                SpecialMoves = new List {
                    "新しいの",
                }
            }
        };
        // データを複製して増やす.
        var breadmen2 = new List(breadmen);
        breadmen.AddRange(breadmen2);
        var breadmen3 = new List(breadmen);
        breadmen.AddRange(breadmen3);
        var breadmen4 = new List(breadmen);
        breadmen.AddRange(breadmen4);
        var breadmen5 = new List(breadmen);
        breadmen.AddRange(breadmen5);

        breadmanNames = breadmen.Select(breadman => breadman.BreadmanModel.name).ToList();
        ~省略~
    }
    ~省略~
}

値を生成するメソッド

検証対象となる、値を生成するメソッドです。

前述の 1. ~ 5. に加えて、2. の Foreach ではなく For を使うもの(検証 2-1)、
さらにそれを逆転させたもの (検証 2-2) を追加しています。

MainController.cs

    // 検証1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す.
    private List GetBreadmanNames_CreatedList() {
        return breadmanNames;
    }
    // 検証2. IEnumerable を使って Iterator メソッドとして返す.
    private IEnumerable GetBreadmanNames_IEnumerable() {
        foreach (var breadman in breadmen) {
            yield return breadman.BreadmanModel.name;
        }
    }
    // 検証3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用).
    private IEnumerable GetBreadmanNames_IEnumerable_Linq() {
        return breadmen.Select(breadman => breadman.BreadmanModel.name);
    }
    // 検証4. 対象の要素だけを持った List をメソッド内で生成し、それを返す.
    private List GetBreadmanNames_Create() {
        var newBreadmanNames = new List();
        foreach (var breadman in breadmen) {
            newBreadmanNames.Add(breadman.BreadmanModel.name);
        }
        return newBreadmanNames;
    }
    // 検証5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用).
    private List GetBreadmanNames_Create_Linq() {
        return breadmen.Select(breadman => breadman.BreadmanModel.name).ToList();
    }
    // 検証2-1. IEnumerable を使って Iterator メソッドとして返す.
    // ForeachではなくForを使って返してみる.
    private IEnumerable GetBreadmanNames_IEnumerable_For() {
        for(var i = 0; i < breadmen.Count; i++) {
            yield return breadmen[i].BreadmanModel.name;
        }
    }
    // 検証2-2. IEnumerable を使って Iterator メソッドとして返す.
    // 順番が逆になってしまうが、速度・GC Allocを計測してみる.
    private IEnumerable GetBreadmanNames_IEnumerable_For_Reverse() {
        for(var i = breadmen.Count - 1; i >= 0; i--) {
            yield return breadmen[i].BreadmanModel.name;
        }
    }

計測する"

ボタンのクリックイベントを利用して計測してみます。

MainController.cs

~省略~
public class MainController : MonoBehaviour {
    ~省略~
    public Button Sample1Button;
    public Button Sample2Button;
    public Button Sample3Button;
    public Button Sample4Button;
    public Button Sample5Button;
    public Button Sample6Button;
    public Button Sample7Button;
    ~省略~
    private void Start() {
        ~省略~
        // 検証1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す.
        Sample1Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach(var breadmanName in GetBreadmanNames_CreatedList()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2. IEnumerable を使って Iterator メソッドとして返す.
        Sample2Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用).
        Sample3Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_Linq()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証4. 対象の要素だけを持った List をメソッド内で生成し、それを返す.
        Sample4Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_Create()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用).
        Sample5Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_Create_Linq()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2-1. IEnumerable を使って Iterator メソッドとして返す(For使用).
        Sample6Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_For()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2-2. IEnumerable を使って Iterator メソッドとして返す(Forを逆転させて使用).
        Sample7Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_For_Reverse()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
    }
    ~省略~

起動 -> ボタンを5回押す -> Profiler の結果を見る 、という内容で確認してみました。

結果

結果は下記の通り。

f:id:mslGt:20171224115042j:plain

やっぱりというか、あらかじめ作成済みの List を返すのが速いようです。

ただ、どのケースを見ても GC Alloc のデータ量は変わらなかったのは少し驚きました。
( List を生成する方がデータ量が多そうだったので)

また、 Foreach や For を使った場合と Linq を使う場合とを比較すると、
最初の一回は Foreach や For を使うほうが速く、それ以降は Linq の方が速い傾向がありました。

ただ速度に関しては、このあと何度か試すと必ずしも上記の通りとはならなかったので、
(検証1. も他と同じくらいの速度になったり)
どの方法を採るか、というのはもう少し検証が必要そうです。

おわりに

少なくとも今回の内容からすると、検証1. の List として持たせるのが速度を最優先するなら良さそうです。

ただ、それぞれを List に、とかし始めるとバグの温床にもなりそうですし、
そもそもデータを格納するためのクラスを作る意味もなくなってくるという。。。

またキャッシュしておくデータの量も増えてしまいますね。

メソッド内で生成する検証2. ~ 検証7. を見ると、ほとんど変わらないように思います。

ということで、検証2. か検証3. の方法で IEnumerable を返して、
呼び出し元で都合が良いように加工する、というのが良さそうです。

やっぱり実際測ってみると意外な結果が見えたりして面白いですね。

最後に、検証のネタに使った Breadman を載せておきます。

www.instagram.com

参照

2017年のお買い物

はじめに

早いものでもう年の瀬ですね。

Twitterなどで2017年に買ったものを紹介している方をちらほら見かけたので、
私も便乗してみます。

なおここで取り上げる基準は、2017年に購入し、そこそこ以上に良いと感じ、かつ私が覚えているものとなります。

良く言えば厳選された内容をお届けしますw

GRAPEVINE「ROADSIDE PROPHET」

GRAPEVINE 20周年となるアルバム。

相変わらず良いです。
突飛なことをするわけでもないけどマンネリ化してるわけでもなく。

今回はミドルテンポの曲が多く、何度も聴く内にじんわりとしみ込んできた感じです。

特に「楽園で遅い朝食」が大好きです。

www.grapevineonline.jp

なぜかディスコグラフィに「ROADSIDE PROPHET」が入ってないので公式ページのトップを貼っておきます。

PC用のバッテリー

ここ2年程はマウスコンピューターのラップトップを使っているのですが、
その交換用のバッテリーを購入しました。

大体充電なしで3, 4時間くらいは使用できるのですが、
長い勉強会などになると心元ない、ということで買い足したという経緯です。

もちろんノート等に書き残すのも良いのですが、
話をメモしつつキーワードを元に調べ物をしつつTweetしつつ、となると難しいので。。。

まだどのぐらいで交換するのがベストかなど試行錯誤しているところですが、
多少長い時間使う場合も、バッテリーを心配しなくて良い、というのはとても良いですね。

なおマウスコンピューターのバッテリー購入は、公式Webページのお問い合わせから。

www.mouse-jp.co.jp

JetBrains All Productsライセンス

これがあれば JetBrains 社の IDE 使い放題!!というやつですね。

C#IDE である Rider の正式版がリリースされたのを機に購入しました。

これと Visual Studioプラグインである ReSharper(C#, C++) 、Java や Kotlin の IntelliJ IDEA は、
すでに無いとコード書くのが厳しいくらい頼りにしています。

まぁあまり依存するのも良くないとは思いつつも、
強力な入力補完で自分の書きたい内容をポンポン書き進められるのは気持ちが良いです。

Effective C#

C# 6 対応版の第三版です。

play.google.com

まだ一度しか読んでいないため「理解した!」とは言い難いのですが、
繰り返し読んでものにできればなぁ、と。

現状では List を返すメソッドで、List をメソッド内で生成して返すのではなく IEnumerable を返して、
メソッド呼び出し元で Foreach で値を取り出す、というのが場合によっては便利だなと感じています。

あとこの本に限りませんが、 Google Play Books だと PC でもスマホでもログインさえすれば見られる、
というのがとても便利です。

家だと大きな画面で見られる方が良いので。

あと英語翻訳でいうと、単語だけでなく熟語も翻訳できるのはとても助かります。

いちいち文字を手打ちで翻訳にかけるのは結構大変なので(;'∀')

難点としては自分で電子書籍をアップロードするときの 200MB 制限がなければなぁ。。。と思うところです。
まぁ自分だけがそれを利用するわけでもないので、仕方がないとは思うのですが。

Office 365 Solo

年間サブスクリプションで購入しました。

もとは OneDrive のストレージ容量を 1TB に増やしたかったからなのですが、
Office 365 も含まれているということで、 PowerPoint でスライド資料を作ったりするのにも活躍してくれています。

やっぱり PC でもスマホでも自動で同期してくれて、最新のものが見られるというのは便利ですね(*'▽')

あと OneDrive は、保存するものがスマホで撮った写真や epub や pdf の電子書籍くらいなので、
ほとんどストレージのことを気にせずに良くなったのがありがたいです。

うかつに PC のローカルフォルダと全データを同期すると、
PC のストレージが死んでしまいかねないのは注意が必要ですが(;'∀')

iPhone X

買いました。

使い勝手はまぁ iPhone だなぁ、という感じで、
画面下から上へのスワイプでホーム画面に戻るなどの独自操作も少しずつ慣れてきている感じです。

特筆すべきは何といっても TrueDepth カメラ。

Unity に ARKit のプラグインがあり、それを使うとかなり簡単に顔のトラッキングができます。

https://www.assetstore.unity3d.com/jp/#!/content/92515

今のところは顔のところに何か三次元ででてきて面白い以上のことはできていないのですが、
例えば顔の向きに合わせてページを移動するとか、 操作 UI などで利用してみても面白そうです。

また、ランニングやサイクリングをトラッキングして Google Fit に送信するとか、
設定時間に近づくと事前に(当日のみ)オフにできる目覚まし機能などは少なくとも標準ではなさそうなので、
Xamarin の学習がてら作ってみても面白いなぁと思っています(実現できるとは言っていない)。

Windows 10 Proライセンス

まだ買ったばかりでほとんど使用できていませんが、
Docker や Hololens の Emulator に Hyper-V が必要、ということだったので Home からアップグレードしました。

今の調子だと今年は手が出せそうにありませんが、
来年頭くらいから色々触ってみたいなと思っています。

おわりに

ざっと思いつくところを挙げてみましたが、結構ありますね。

ソフトウェアやライセンスが多い感じ。

掛けた費用を回収するという意味でも、来年もバリバリ使って楽しんでいきたいと思います。

Xamarin.FormsでXAMLと仲良くしたい話 その2

はじめに

この記事は [初心者さん・学生さん大歓迎!] Xamarin その1 Advent Calendar 2017 の15日目の記事です。

qiita.com

そろそろ終盤にさしかかってきました。
最初は無理かな~とも思っていましたが、ここまではちゃんと続いていてすごいです!

ここまできたら、できれば最後まで完走したいですねぇ。

さて今回は、前回の続きとして RelativeLayoutに触れてみたいと思います。

サンプルを見てみる

RelativeLayout は、 Android の ConstraintLayout や iOS の AutoLayout のように、
要素の表示位置やサイズに制約を付けてレイアウトを組むものです。

といっても分かりづらいので、公式のサンプルを見てみます。

https://developer.xamarin.com/guides/xamarin-forms/user-interface/layouts/relative-layout/

< RelativeLayout>
    < BoxView Color="Red" x:Name="redBox"
        RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToParent,
            Property=Height,Factor=.15,Constant=0}"
        RelativeLayout.WidthConstraint="{ConstraintExpression
            Type=RelativeToParent,Property=Width,Factor=1,Constant=0}"
        RelativeLayout.HeightConstraint="{ConstraintExpression
            Type=RelativeToParent,Property=Height,Factor=.8,Constant=0}" />
    < BoxView Color="Blue"
        RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=redBox,Property=Y,Factor=1,Constant=20}"
        RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=redBox,Property=X,Factor=1,Constant=20}"
        RelativeLayout.WidthConstraint="{ConstraintExpression
            Type=RelativeToParent,Property=Width,Factor=.5,Constant=0}"
        RelativeLayout.HeightConstraint="{ConstraintExpression
            Type=RelativeToParent,Property=Height,Factor=.5,Constant=0}" />
< /RelativeLayout>

ぎょえ~って感じがしますが、一つ一つ見てみましょう。

各 BoxView には大きく分けて4つの制約が付与されています。

  • XConstraint : X 軸方向の表示位置の制約
  • YConstraint : Y 軸方向の表示位置の制約
  • WidthConstraint : 幅の制約
  • HeightConstraint : 高さの制約

なおこれらの制約は順番を変えても問題ないようです。

また全ての制約を付与する必要はなく、いくつかだけを指定することも可能です。
(一つも指定しない場合は左上にデフォルトのサイズ(たぶん)で表示されました)

で、それぞれの制約の中を見てみると、同じような項目が設定されています。

  • ConstraintExpression
  • Type
  • ElementName (下のBoxViewのみ)
  • Property
  • Factor
  • Constant

ConstraintExpression は制約を付与する式を定義するよ~、というためのものだと思いますので、
それ以外の項目について見てみます。

Type

その要素の位置やサイズを、何をベースに決めるかを指定します。

指定できるのは下記の2種類です。

  • RelativeToParent : 親要素をベースにする
  • RelativeToView : 特定の要素をベースにする(要素の指定は ElementName で行う)

XConstraint、 YConstraint、 WidthConstraint、 HeightConstraint の四つをどちらかに統一する必要はなく、
それぞれ指定することができます。

ElementName

ちょっと順番をとばして ElementName です。

Type で書いた通り、Type を RelativeToView にしている場合に、ベースとなる要素を指定します。

ElementName が無い場合

その制約を指定しない場合と同じ状態で表示されます。

Type を RelativeToParent にして ElementName を指定した場合

特に変化はなさそうです。

存在しない要素名を ElementName に指定した場合

実行時に unhandled exception が発生します。

循環参照

2つの要素の Type を RelativeToView にして、ElementName にお互いの名前を指定した場合、
実行時に unhandled exception が発生します。

自分の名前を ElementName に指定した場合

実行時に unhandled exception が発生します。

ElementName の間違いによるエラーは、特に複雑なレイアウトの場合はやらかしがちだと思うので、
注意が必要ですね。。。

Property

制約のベースとなる親要素のプロパティを指定します。

例えば上の BoxView (RedBox) の子となる下の BoxView (BlueBox) を、
RedBox の右下に配置したい場合、下記のように書くことができます。

< RelativeLayout x:Name="Outline">
    < BoxView Color="Red" x:Name="RedBox"
        RelativeLayout.WidthConstraint="{ConstraintExpression
        Type=RelativeToParent,Property=Width,Factor=0.5,Constant=0}"
        RelativeLayout.HeightConstraint="{ConstraintExpression
        Type=RelativeToParent,Property=Height,Factor=0.5,Constant=0}" />

    < !-- X座標値をRedBoxのWidthに、Y座標値をRedBoxのHeightにする -->  
    < BoxView Color="Blue" x:Name="BlueBox"              
        RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView,
        ElementName=RedBox, Property=Height, Factor=1,Constant=0}"
        RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView,
        ElementName=RedBox, Property=Width, Factor=1,Constant=0}" />
< /RelativeLayout>

結果は以下の通りです。

f:id:mslGt:20171215012715j:plain

Factor

Property で指定した親要素の値 * Factor + Constant の値が、子要素の値として設定されます。

例えば XConstraint で、Type=RelativeToParent、 Property=Width、 Factor=0.5 とした場合、
親要素の Width の 0.5 倍の値が X 座標値として設定されます。

マイナス、1より大きい数を指定することもできます。
ただし、当然ながら親要素が0の場合は変わりません。

Constant

オフセット値です。

マイナスの値を指定することもできます。

Width、 Height では、 0より大きい値を指定すると要素は大きくなり、
0未満の値を入れると小さくなります。

レイアウトしてみる

なんとなく並べてみました。

< ?xml version="1.0" encoding="utf-8" ?>
< ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSample.View.MainPage">
    < RelativeLayout x:Name="Outline">
        < !--これをベースに他の要素を配置 -->
        < BoxView Color="Red" x:Name="RedBox"
                 RelativeLayout.WidthConstraint="{ConstraintExpression
            Type=RelativeToParent,Property=Width,Factor=0,Constant=100}"
                 RelativeLayout.HeightConstraint="{ConstraintExpression
            Type=RelativeToParent,Property=Height,Factor=0,Constant=100}" />

        < BoxView Color="Blue" x:Name="BlueBox"
                 RelativeLayout.WidthConstraint="{ConstraintExpression
            Type=RelativeToView,Property=Width,Factor=1,ElementName=RedBox}"
                 RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Height,Factor=1}"
                 RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Width,Factor=1}"
                 RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Height,Factor=1}" />

        < BoxView Color="Yellow" x:Name="YellowBox"
                 RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox, Property=Width,Factor=1}"
                 RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Height,Factor=1}"
                 RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Height,Factor=2}"
                 RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Width,Factor=2}" />
        
        < BoxView Color="Green" x:Name="GreenBox"
                 RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox, Property=Width,Factor=1}"
                 RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Height,Factor=1}"
                 RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Height,Factor=3}"
                 RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView,
            ElementName=RedBox,Property=Width,Factor=3}" />
        
        < Button BackgroundColor="Aquamarine" BorderRadius="180" Opacity="0.5"
                Text="世界さんちーっす"
                RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToParent,
            Property=Width, Factor=0.3}"
                RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToParent,
            Property=Width, Factor=0.3}"
                RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToParent,
            Property=Width,Factor=0.4, Constant=0}"
                RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToParent,
            Property=Height,Factor=0.3, Constant=0}"/>
    < /RelativeLayout>
< /ContentPage>
  • ボタンでは BorderRadius で角を丸めることができ、180 にすると丸くすることができます。
    ただし上記の状態だと、ハイライト時は四角く表示されてしまいます。
  • Opacity を指定することで、透明度を設定することができます。
    値は 0.0 ~ 1.0 です。
  • ボタンの位置を画面中央にするのは、(画面サイズ / 2) - (ボタンサイズ / 2) の計算が必要なため、
    XAMLではなく、コードビハインドで指定する必要があるようです。
    (Constant に上記の書いたところ、エラーが発生しました)

f:id:mslGt:20171215013242j:plain

おわりに

親要素のサイズ・位置を決めてそれをベースに他の要素をレイアウトする RelativeLayout は、
特に多様なディスプレイサイズへの対応が必要となるであろう Xamarin.Forms で大活躍してくれそうです。

とはいえデザインが固まる前から制約を正しく付与していくのはなかなか大変そうだったり(慣れの問題かもですが)、
内容によっては前回試していた StackLayout や GridView の方がシンプルに書けることもあると思うので、
上手く使い分けていきたいところです。

さて、アドベントカレンダー、明日は yakumomo さんです。

よろしくお願いいたします~m(__)m。

参照

Xamarin.FormsでXAMLと仲良くしたい話 その1

はじめに

この記事は [初心者さん・学生さん大歓迎!] Xamarin その1 Advent Calendar 2017 の14日目の記事です。

qiita.com

とりあえず起動はするし、ボタンを押すとHello Worldと出力するとか、
簡単な動きくらいは作れる気がしてきました。

しかしながら、見た目がどうにもなのは悲しい。

ということで、今回は UI の部分について調べてみることにしました。

なお、 Xamarin.Forms ではボタンなどをコードで追加することもできますが、
今回は XAML を中心に進めてみます。

デフォルトで生成されるXAMLを見てみる

まず Blank App でプロジェクトを作成して、デフォルトの XAML を見てみます。

MainPage.xaml

< ?xml version="1.0" encoding="utf-8" ?>
< ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSample"
             x:Class="XamlSample.MainPage">

    < Label Text="Welcome to Xamarin.Forms!" 
           VerticalOptions="Center" 
           HorizontalOptions="Center" />

< /ContentPage>
  • 「xmlns:local="clr-namespace:XamlSample"」の行は、(少なくとも)デフォルトでは使用していないようで、
    削除してしまっても特にエラーは起きませんでした。
  • この記事を書く少し前のバージョンまで Label は ContentPage.Content と StackLayout の中に入れられていましたが、
    仕様の変更があったようです。( PCL から .Net Standard に変更したことには関係がなさそうなので)

ContentPage に対して コードの表示 を行うと、 コードビハインド (MainPage.xaml.cs) が開きます。

要は XAML ファイルのルート要素とコードビハインドのクラスとが紐づいているので( x:Class="XamlSample.MainPage" で指定)、
コードから XAML 要素にアクセスすることが可能になるのですね。

Xamarin.Forms で使用できる Layout は下記の通り。

ボタンなどのコントロール類は、 WPF などと(多分)ほぼ同じです。
自動で各プラットフォームらしい見た目にしてくれるのはすごいですね!

xmlnsとxmlns:xについて

「xmlns="http://xamarin.com/schemas/2014/forms"」で XAML の使用を宣言しています。

「xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"」で「x:Class」や「x:Name」などのプロパティが使用可能になります。

x は変更可能で、「xmlns:x= ~ 」を「xmlns:xx= ~ 」のように変更すると「xx:Class」と書くことができるようになります。

また xmlns:x では Name など XML でも定義されているプロパティがありますが、
これはそのプロパティをどの要素に対しても使用可能にするために定義されているようです。

ContentPage直下のLayout

適当にいじっていた時におや?と思ったところですが、 ContentPage 直下におけるLayoutは一つだけのようです。
そのため、下記のように書いてしまうと、上に書いた StackLayout の内容が表示されませんでした。

< ?xml version="1.0" encoding="utf-8" ?>
< ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSample.View.MainPage">
    < StackLayout>
        < Label Text="Welcome to Xamarin.Forms!" 
           VerticalOptions="Center" 
           HorizontalOptions="Center" />
    < /StackLayout>
    < AbsoluteLayout>
            < BoxView WidthRequest="100" HeightRequest="100" BackgroundColor="YellowGreen"/>
    < /AbsoluteLayout>
< /ContentPage>

コードで要素を追加する

例えば Layout 内の要素は決まっていて、中のデータだけが変化する場合は DataBinding を使うのが良さそうです。

ただ、条件によって要素ごと差し替えたい場合だと難しそうです。

ということで、コードから要素を追加してみます。

追加対象の親要素を取得する

まずは要素を追加する対象となる、親の要素を取得してみます。

前述の通り、 XAML ファイルの要素とコードビハインドのクラスは紐付けられており、
コードビハインドで this.Content とすれば、 XAML ファイルの ContentPage.Content にアクセスできます。

ということで、あとは対象の要素を指定して、そこに子となる要素を追加するだけです。

MainPage.xaml

< ?xml version="1.0" encoding="utf-8" ?>
< ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSample.MainPage">
    < StackLayout x:Name="Outline">
        < Label Text="Welcome to Xamarin.Forms!"
            VerticalOptions="CenterAndExpand" 
            HorizontalOptions="CenterAndExpand" />
    < /StackLayout>
< /ContentPage>

※2017.12.14 21:30更新

親要素を取得するのに FindByName を使用していましたが、
XAML で 親要素に x:Name で名前を付けている場合、
そのまま x:Name でつけた名前でコードビハインドでもアクセスできるそうです。

処理も減ってシンプルになりますね(*'▽')

田淵さん、ありがとうございますm(__)m

Before

// 親要素となる StackLayout.
var layout = this.FindByName< StackLayout>("Outline");
layout.Children.Add(addContent);

After

// 親要素となる StackLayout.
Outline.Children.Add(addContent);

MainPage.xaml.cs

using Xamarin.Forms;

namespace XamlSample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

            // 追加する要素.
            var addContent = new Label
            {
                Text = "Label from code",
                VerticalOptions = LayoutOptions.CenterAndExpand,
                HorizontalOptions = LayoutOptions.CenterAndExpand,
            };
            // 親要素となる StackLayoutに要素を追加.
            Outline.Children.Add(addContent);
        }
    }
}
  • これで「Welcome to Xamarin.Forms!」というラベルの下に、
    「Label from code」というラベルが追加されます。

レイアウトしてみる StackLayout

それでは、いくつか Layout を試してみます。

StackLayout

  • 縦方向または横方向に要素を並べます (Orientation で切り替え)。
  • 何も指定しない場合、縦方向に並べる場合は横幅いっぱい、横方向の場合は縦幅いっぱいに要素が表示されます。
  • StackLayout だけを使って縦横組み合わせたい場合は、 StackLayout を入れ子構造にします。
  • ただし縦方向に並べる StackLayout と横方向に並べる StackLayout を同階層に置いてしまうとエラーになります。

< ?xml version="1.0" encoding="utf-8" ?>
< ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSample.View.MainPage">
    < StackLayout BackgroundColor="Azure" Orientation="Vertical" VerticalOptions="Start">
        < StackLayout Orientation="Horizontal" Spacing="0">
            < BoxView BackgroundColor="Yellow" WidthRequest="100" />
            < StackLayout Orientation="Vertical" BackgroundColor="Brown" HorizontalOptions="FillAndExpand">
                < Label Text="世界さん" TextColor="White"/>
                < Label Text="ちーっす!" TextColor="White" />
            < /StackLayout>
        < /StackLayout>
        < StackLayout Orientation="Horizontal" Spacing="0">
            < BoxView BackgroundColor="Yellow" WidthRequest="100" />
            < StackLayout Orientation="Vertical" BackgroundColor="Brown" HorizontalOptions="FillAndExpand" Spacing="0">
                < Label Text="Hello" TextColor="White" Margin="3, 3, 3, 3"/>
                < Label Text="World!" TextColor="White" Margin="3" />
            < /StackLayout>
        < /StackLayout>
    < /StackLayout>
< /ContentPage>
  • 要素同士はデフォルトでスペースが含まれるので、なくしたい場合は Spacing を "0" にする必要があります。

結果はこんな感じになります。

f:id:mslGt:20171214000906j:plain

レイアウトしてみる GridView

次は格子状にレイアウトできる GridView を試してみます。

GridView では行数・列数とサイズを指定し、要素と配置する場所を指定します。

< ?xml version="1.0" encoding="utf-8" ?>
< ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSample.View.MainPage">
    < Grid VerticalOptions="FillAndExpand">
        
        < !-- 行の指定. 今回は一番下の行が30固定、残りが半分ずつ使うことになります -->
        < Grid.RowDefinitions >
            < RowDefinition Height="1*"/>
            < RowDefinition Height="Auto"/>
            < RowDefinition Height="30"/>
        < /Grid.RowDefinitions >
        < !-- 列の指定. Widthを指定していないので3分割 -->
        < Grid.ColumnDefinitions>
            < ColumnDefinition />
            < ColumnDefinition />
        < /Grid.ColumnDefinitions>
        < !-- StackLayoutを入れたり-->
        < StackLayout VerticalOptions="FillAndExpand"
                     HorizontalOptions="FillAndExpand"
                     BackgroundColor="Yellow">
            < Label Text="世界さんちーっす" />
        < /StackLayout>
        < !-- GridViewを入れたり-->
        < Grid VerticalOptions="FillAndExpand"
                  Grid.Row="0"
                  Grid.Column="1" >
            < Grid.RowDefinitions >
                < RowDefinition Height="Auto"/>
                < RowDefinition Height="Auto"/>
            < /Grid.RowDefinitions >
            < Grid.ColumnDefinitions>
                < ColumnDefinition />
                < ColumnDefinition />
            < /Grid.ColumnDefinitions>
            < BoxView Color="Aqua" 
                     Grid.Row="0"
                     Grid.Column="0" />
            < BoxView Color="Maroon" 
                     Grid.Row="1"
                     Grid.Column="0" />
            < BoxView Color="Navy" 
                     Grid.Row="1"
                     Grid.Column="1" />
            < BoxView Color="Silver" 
                     Grid.Row="0"
                     Grid.Column="1" />
        < /Grid>
        < BoxView Color="Maroon" 
                 Grid.Row="1"
                 Grid.Column="0" />
        < BoxView Color="Navy" 
                 Grid.Row="1"
                 Grid.Column="1" />
        < BoxView Color="Purple" 
                 Grid.Row="2"
                 Grid.Column="0" />
        < BoxView Color="Lime" 
                 Grid.Row="2"
                 Grid.Column="1" />
    < /Grid>  
< /ContentPage>
  • Grid.Row 、Grid.Column を指定しない場合、デフォルトで Grid.Row: 0 、Grid.Column: 0 が設定されるため、
    複数あると最初の要素が隠れてしまいます。

上記のコードを実行するとこのようになります。

f:id:mslGt:20171214001007j:plain

複雑なレイアウトを行う場合は、行番号・列番号の指定がややこしそうですね。。。

BoxViewについて

これまでも登場している BoxView ですが、これは四角形を表示するものです。

GridView などと同じように、中に Label などの要素を入れられるのかな?と思いきやエラーになります。

エラーになる

< BoxView Color="Silver">
    < Label Text="世界さん、ちーっす" />
< /BoxView>

なのであくまでも四角形を表示するためのもの、ということのようです。

装飾などに利用するのでしょうか。

おわりに

とりあえず StackLayout と GridView 、 BoxView に触れてみましたが、
まだ「なんでこんな大きさになるの。。。(´・ω・`)」となることも多いです。

まぁこういうのは慣れが重要だと思いますので、実際にアプリを作りながら慣れていきたいと思います。

次回は入りきらなかった RelativeLayout の予定。。。のはず。

参照

Xamarin.Forms

XAML