ディーバ Blog

大阪発 C#の会社、株式会社ディーバの Blog です。

Xamarin.iOS 透明な NavigationBar と Toolbar

Xamarin に限らず iOS の一般的な話と同じですが、Xamarin.iOS で NavigationBar と Toolbar の背景を透明にします。

背景を透明にするだけでは、NavigationBar の下、Toolbar の上部分に線が表示されるので、それも非表示にします。

f:id:jz5_diva:20161026115947p:plain

public override void ViewWillAppear(bool animated)
{
    base.ViewWillAppear(animated);

    this.NavigationController.NavigationBar.TintColor = UIColor.White;
    this.NavigationController.NavigationBar.TitleTextAttributes = new UIStringAttributes()
    {
        ForegroundColor = UIColor.White
    };
    this.NavigationController.NavigationBar.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); // 背景透明
    this.NavigationController.NavigationBar.ShadowImage = new UIImage(); // 境界線透明

    this.NavigationController.Toolbar.TintColor = UIColor.White;
    this.NavigationController.Toolbar.SetBackgroundImage(new UIImage(), UIToolbarPosition.Any, UIBarMetrics.Default); // 背景透明
    this.NavigationController.Toolbar.ClipsToBounds = true; // 境界線非表示
}

Xamarin.iOS でフォルダー選択の UI (TreeView) を作る

iOS には、階層構造を表現・選択する UI コントロールが提供されていません。

検索すると、Xamarin iOS – Create custom TreeView control for iPad / iPhone « Milen's Blog で実装しているコードがありましたが、きちんと動作していない感じだったので、作って見ました。コードは、diva-osaka/xamarin-ios-treeviewsample にあります。

f:id:jz5_diva:20161020130040p:plain

フォルダーを閉じると、これまで開いていた情報は忘れます(再度 開くと直下の子要素しか表示しません)。

ノートを表すクラス。とりあえず表示と動作に必要なプロパティしかありません。

using System.Collections.Generic;

namespace TreeViewSample
{
    public class TreeNode
    {
        public List<TreeNode> Children { get; set; } = new List<TreeNode>();

        public string Name { get; set; }

        public int Level { get; set; }

        public bool IsExpanded { get; set; }
    }
}

UITableViewController を元に作成。

using Foundation;
using System;
using System.Collections.Generic;
using System.Linq;
using UIKit;

namespace TreeViewSample
{
    public partial class FolderViewController : UITableViewController
    {
        public FolderViewController(IntPtr handle) : base(handle)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            TableView.RowHeight = FolderCell.Height;
            TableView.SeparatorColor = UIColor.Clear;
            TableView.Source = new TreeViewSource(CreateNodes());
        }

        private TreeNode CreateNodes()
        {
            var osaka = new TreeNode { Level = 0, Name = "大阪市" };

            var sub1 = new TreeNode { Level = 1, Name = "中央区" };
            var sub2 = new TreeNode { Level = 1, Name = "北区" };
            var sub3 = new TreeNode { Level = 1, Name = "西区" };

            osaka.Children.Add(sub1);
            osaka.Children.Add(sub2);
            osaka.Children.Add(sub3);

            var sub4 = new TreeNode { Level = 2, Name = "淡路町" };
            sub1.Children.Add(sub4);

            return osaka;
        }

        private class TreeViewSource : UITableViewSource
        {
            private List<TreeNode> Nodes = new List<TreeNode>();

            public TreeViewSource(TreeNode root)
            {
                Nodes.Add(root);
            }

            public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
            {
                var cell = tableView.DequeueReusableCell("FolderCell") as FolderCell;
                cell.SetCellContents(Nodes[indexPath.Row]);
                return cell;
            }

            public override nint RowsInSection(UITableView tableview, nint section)
            {
                return Nodes.Count;
            }

            public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
            {
                var selectedNode = Nodes[indexPath.Row];
                var selectedIndex = indexPath.Row;

                if (!selectedNode.IsExpanded)
                {
                    var children = selectedNode.Children;

                    // 開く
                    if (!children.Any())
                    {
                        return;
                    }

                    selectedNode.IsExpanded = true;

                    var indexPaths = new List<NSIndexPath>();
                    foreach (var node in children.Select((Value, Index) => new { Value, Index }))
                    {
                        node.Value.IsExpanded = false;
                        indexPaths.Add(NSIndexPath.FromRowSection(selectedIndex + node.Index + 1, 0));
                    }
                    Nodes.InsertRange(selectedIndex + 1, children);
                    tableView.InsertRows(indexPaths.ToArray(), UITableViewRowAnimation.Automatic);
                }
                else
                {
                    // 閉じる
                    selectedNode.IsExpanded = false;

                    var node = Nodes.Skip(selectedIndex + 1).FirstOrDefault(i => i.Level <= selectedNode.Level);
                    var deleteCount = (node != null) ?
                        Nodes.IndexOf(node) - selectedIndex - 1 :
                        Nodes.Count - selectedIndex - 1;

                    var indexPaths = new List<NSIndexPath>();
                    for (int i = 0; i < deleteCount; i++)
                    {
                        Nodes.RemoveAt(selectedIndex + 1);
                        tableView.DeleteRows(new NSIndexPath[] { NSIndexPath.FromRowSection(selectedIndex + 1, 0) }, UITableViewRowAnimation.Top);
                    }
                }
            }
        }
    }
}
using CoreGraphics;
using Foundation;
using System;
using UIKit;

namespace TreeViewSample
{
    [Register("FolderCell")]
    public class FolderCell : UITableViewCell
    {
        static readonly public int Height = 52;

        private int level;
        private UIImageView imageView;
        private UILabel titleLabel;

        public FolderCell(IntPtr handle) : base(handle)
        {
        }

        void ReleaseDesignerOutlets()
        {
        }

        public void SetCellContents(TreeNode node)
        {
            level = node.Level;

            imageView = new UIImageView
            {
                Image = UIImage.FromFile("Folder.png"),
                ContentMode = UIViewContentMode.Left
            };
            imageView.SizeToFit();

            titleLabel = new UILabel()
            {
                TextColor = UIColor.Black,
                BackgroundColor = UIColor.Clear,
                Text = node.Name
            };
            titleLabel.SizeToFit();

            foreach (var v in ContentView.Subviews)
            {
                v.RemoveFromSuperview();
            }

            ContentView.AddSubviews(imageView, titleLabel);
        }

        public override void LayoutSubviews()
        {
            base.LayoutSubviews();

            var indent = 28;
            var margin = 8;

            var imageFrame = new CGRect(level * indent + margin, 0, 28.0f, Height);
            imageView.Frame = imageFrame;

            var titleFrame = new CGRect(level * indent + imageFrame.Width + margin * 2, 0, (float)titleLabel.Bounds.Width, Height);
            titleLabel.Frame = titleFrame;
        }
    }
}

UICollectionViewController の Cell サイズを実行時に変更

Xamarin.iOS のUICollectionViewController を継承した ViewController 使用時、コードで Cell サイズを動的に変更する方法です。

Layout プロパティの ItemSize を設定します。

public partial class SampleViewController : UICollectionViewController
{
    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        var w = CollectionView.Frame.Width / 3;
        (Layout as UICollectionViewFlowLayout).ItemSize = new CGSize(w, w);
    }
}

共通のコードで iOS/Android/UWP のダイアログを表示できる ACR User Dialogs for Xamarin and Windows

ACR User Dialogs for Xamarin and Windows

ACR User Dialogs for Xamarin and Windows を使うと、MvvmCrossMVVM パターンの ViewModel のコード(PCL プロジェクト側)から、ダイアログを表示するコードを簡単に書けます。MvvmCross 用のプラグインもあり MvvmCross のサイトからも紹介されいています

ダイアログ (Alert) だけでなく、ローディング画面なども呼び出せます。機能は次の通り。

  • Action Sheet (multiple choice menu)
  • Alert
  • Confirm
  • Loading
  • Login
  • Progress
  • Prompt
  • Toasts

NuGet からプロジェクトにインストールします。ViewModel 側(PCL プロジェクト)と、View 側の各プラットフォームのプロジェクトそれぞれにインストールします。

公式のサンプルコードは GitHub の examples にあります。

Dialog

using using Acr.UserDialogs;

UserDialogs.Instance.Alert("たこ焼きです", "確認", "OK");

f:id:jz5_diva:20160421102435p:plain

Action Sheet

using using Acr.UserDialogs;

var config = new ActionSheetConfig();
config.Title = "メニュー";
config.Add("たこ焼き", () => {/* Do something */ });
config.Add("お好み焼き", () => {/* Do something */ });
UserDialogs.Instance.ActionSheet(config);

f:id:jz5_diva:20160421103041p:plain

Confirm

using using Acr.UserDialogs;

var config = new ConfirmConfig();
config.Title = "確認";
config.Message = "たこ焼きを購入します";
config.OnConfirm = (result) =>
{
    /* Do something */
};
config.OkText = "購入";
config.CancelText = "キャンセル";
UserDialogs.Instance.Confirm(config);

f:id:jz5_diva:20160421102456p:plain

おわりに

上記以外にももう少し細かなオプションや、非同期呼び出しなども用意されています。使用感として、View 側に一切コードが不要のため手軽で便利ですが、Android ではスタイルを適用できないなど、少しこだわったアプリでは使いづらいところもあります。

Xamarin.iOS の Google Maps で独自マーカーと情報ウィンドウの表示

Xamarin.iOS の Google Maps でマーカー画像の変更と、カスタムした情報ウィンドウ (InfoWindow) の表示方法です。

Google Maps の表示方法 はこちらから。

マーカーと情報ウィンドウの表示

デフォルトのマーカーと情報ウィンドウの場合。Title・Snippet プロパティを指定すれば情報ウィンドウもタップ時に表示されます。

var marker1 = new Marker()
{
    Title = "株式会社ディーバ",
    Snippet = "大阪発 C# の会社",
    Position = new CLLocationCoordinate2D(34.686564, 135.503155),
    Map = mapView
};

f:id:diva_osaka:20160419162542p:plain

マーカーの画像の変更

Icon プロパティを変更するだけです。InfoWindowAnchor プロパティで、情報ウィンドウの表示位置を調整できます。

var marker1 = new Marker()
{
    Title = "株式会社ディーバ",
    Snippet = "大阪発 C#の会社",
    Position = new CLLocationCoordinate2D(34.686564, 135.503155),
    Icon = UIImage.FromBundle("ic_place_48pt"),
    InfoWindowAnchor = new CGPoint(0.5, 0),
    Map = mapView
};

f:id:diva_osaka:20160419162552p:plain

カスタムした情報ウィンドウの表示

すべて置き換え

情報ウィンドウすべてを独自に置き換える場合は、MarkerInfoWindow プロパティを使って View を返すメソッドを指定します。

mapView.MarkerInfoWindow = new GMSInfoFor((view, marker) =>
{
    var label = new UILabel();
    label.Text = marker.Title;
    label.BackgroundColor = UIColor.Red;
    label.SizeToFit();
    return label;
});

f:id:diva_osaka:20160419164322p:plain

xib ファイルに見た目を定義して、View として返すには、次のようになります(参考: Using the View .xib Template - Xamarin)。

mapView.MarkerInfoContents = new GMSInfoFor((view, marker) =>
{
    var arr = NSBundle.MainBundle.LoadNib("InfoWindowView", null, null);
    return Runtime.GetNSObject<UIView>(arr.ValueAt(0));
});

中身のみ置き換え

情報ウィンドウの中身のみ独自に置き換える場合は、MarkerInfoContents プロパティを使って View を返すメソッドを指定しますが、吹き出しが隠れてしまうことがあり、いい感じにできません。

mapView.MarkerInfoContents = new GMSInfoFor((view, marker) =>
{
    var v = new UIView();
    v.Frame = new CGRect(0, 0, 200, 100);
    v.BackgroundColor = UIColor.Red;
    return v;
});

f:id:diva_osaka:20160419165306p:plain

解決方法は、不明です。View が正方形であれば(?)、正しく吹き出し内に表示されるようです。

Xamarin.iOS で Google Maps の表示

Google Maps SDK for iOS を利用できる Xamarin.Google.iOS.Maps ライブラリを NuGet からインストールします。

古くから Xamarin 用コンポーネントがあったようですが、一時期 Google Maps のアップデートに対応できず使えない状態だったようです。

API Key の設定

Google Developer Console から Google Maps SDK for iOS を有効にし、iOS API キーを取得します。

キーは、AppDelegate 内で設定します。

using Google.Maps;

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    MapServices.ProvideAPIKey("iOS API Key");
    ...
}

地図の表示

ViewController 内で生成して View に表示します。

using Google.Maps;
using CoreGraphics;

private MapView mapView;

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    var camera = CameraPosition.FromCamera(34.686564, 135.503155, 6);
    mapView = MapView.FromCamera(CGRect.Empty, camera);

    View = mapView;
}

古いコードサンプルでは、mapView の StartRendering, StopRendering メソッドを記載していますが、deprecated のメソッドで不要のようです。

マーカーの表示

var marker1 = new Marker()
{
    Title = "株式会社ディーバ",
    Position = new CLLocationCoordinate2D(34.686564, 135.503155),
    Map = mapView
};

f:id:jz5_diva:20160419102955p:plain

リンク