Xamarin.iOS で FontAwesome を使う

デザインが苦手な自分みたいな開発者にとって、FontAwesome は非常に助かる存在。 少なくともアイコンに関しては悩むことがなくなる。 そんなステキなアイコンフォントセットを、Web だけでなくアプリの開発でも使いたい、と考えるのは自然なことだと思う。

Xamarin.iOS でも FontAwesome は使える。そう Iconize ならね。

www.nuget.org

NuGet でパッケージをインストールしたら、AppDelegate に初期化処理を追加する。

public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
    Plugin.Iconize.Iconize.With(new Plugin.Iconize.Fonts.FontAwesomeModule());

    return true;
}

パッケージに含まれているアイコンフォントをアプリから使えるようにするために、Info.plist に UIAppFont も追加しないといけない。 よく忘れがちなので注意。

<key>UIAppFonts</key>
<array>
  <string>iconize-fontawesome.ttf</string>
</array>

あとは使うだけ。FontAwesome のアイコンフォントを UIImage に変換してから、タブやツールバーに表示するのをよくやる。

using System;
using UIKit;
using Plugin.Iconize;
using Plugin.Iconize.iOS;

namespace FontAwesomeSample.ViewControllers
{
    public class GitHubViewController : UITableViewController
    {
        public GitHubViewController()
        {
            Title = "GitHub";

            // GitHub のアイコンフォントを UIImage 化してタブに表示
            var icon = Iconize.FindIconForKey("fa-github");
            var image = icon.ToUIImage(28);
            TabBarItem = new UITabBarItem()
            {
                Title = "GitHub",
                Image = image,
            };
        }
    }
}

実行すると、ちゃんとタブに GitHub アイコンが表示されている。

f:id:griefworker:20171014231954p:plain

UILabel や UIButton を拡張したコントロールも提供されてはいるけど、UIImage に変換するのが一番使い勝手がいい。

『メタプログラミング.NET』を読んだ

メタプログラミングと聞くと、LISPRubyJavaScript といったスクリプト言語を思い浮かべる人が多いと思う。 でも、メタプログラミングスクリプト言語の専売特許というわけではない。C# のような静的型付けの言語でも可能だということを、本書は示している。 T4 を使えばソースコードを生成できるし、式や Reflection.Emit で動的にメソッドを作成して実行することも可能。 ビルド後にアセンブリを書き換えて、横断的な処理を追加することだってできる。そのためのツールは提供されている。Roslyn を使い、動的にソースコードの文字列をコンパイルして実行できるところまできていて、RubyJavaScript で行うメタプログラミングにかなり近づいた。

ただ、2013年の本なので、今となっては参考程度にとどめておいたほうがいい内容もまぁまぁある。 DLR で出てくる IronPythonIronRuby といった Iron 言語は、自分の観測範囲で名前を聞かなくなって久しい。Boo も。 Spring.NET は、本家である Java の Spring の進化についていけず、置きざりにされてしまった感が拭えない。

自分は社内向けにライブラリを書いたりする仕事が多いので、T4 でソースコードを生成したり、式で動的にメソッドを作成して実行したりするコードは書いてきた。 CodeDom は大昔に少し使っていたが、ソースコード生成どまりで、動的にアセンブリを作成して実行するまではやってない。 それに T4 使うようになって出番なし。 Reflection.Emit や Cecil を使って IL を書くのは、まだ本格的にやってない。 IL までいくと、いよいよ黒魔術だ。 ただ、黒魔術に手を染めたいと思い始めた。 C#ラムダ式を書いてデバッグ実行し、式木の構造を調べるのはよくやるが、 IL でも似たアプローチができるのは目から鱗だったので。 確かに、書いたコードがどんな IL に変換されるのか、ILSpy を使えば簡単に調べることができる。 この TIPS は本書で1番の収穫かもしれない。

本書は .NET の黒魔術書と言ってもよさそうだ。

メタプログラミング.NET (アスキー書籍)

メタプログラミング.NET (アスキー書籍)

PostgreSQL で緯度経度から住所を検索する

先日、SQL Server で緯度経度から住所を検索する方法を試したが、 PostgreSQL でも拡張の PostGIS を使えば同じようなことが可能だった。

-- 住所を格納するテーブルを作成
CREATE TABLE addresses (
  address_id BIGSERIAL PRIMARY KEY,
  address TEXT,
  location GEOMETRY(POINT, 4326)
);

-- 空間インデックスを作成
CREATE INDEX ON addresses USING gist (ST_Transform(location, 32654));

-- テストデータを投入
INSERT INTO addresses
(
  address,
  location
)
VALUES (
  '福岡県福岡市中央区天神一丁目',
  ST_GeomFromText('POINT(130.401396 33.590878)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  '福岡県福岡市中央区天神二丁目',
  ST_GeomFromText('POINT(130.398043 33.589390)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  '福岡県福岡市中央区天神三丁目',
  ST_GeomFromText('POINT(130.396224 33.593515)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  '福岡県福岡市中央区天神四丁目',
  ST_GeomFromText('POINT(130.399850 33.594580)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  '福岡県福岡市中央区天神五丁目',
  ST_GeomFromText('POINT(130.398918 33.596781)', 4326)
);

-- 天神二丁目の2の緯度経度に一番近い住所を取得
SELECT
  address_id,
  address,
  ST_Transform(location, 32654) <-> ST_Transform(ST_GeomFromText('POINT(130.398821 33.589085)', 4326), 32654) AS distance
FROM
  addresses 
ORDER BY
  distance
LIMIT 1;

Amazon RDS for PostgreSQL だと、最初から PostGIS が有効になっているのですぐ使えた。

『3月のライオン(13)』を読んだ

13巻の主役は二海堂だった。 主人公の桐山はほとんど見せ場なし。 二海堂は登場するたびに、そのひたむきさとふくふくさに癒されてたけど、 今回は宗谷名人とのガチ対局。 二海堂のガチ対局が描かれるのっていつ以来だろう。かなり久しぶり。 一手損角換わりのワクチンを編み出すどころか、むしろ魅了されてしまっていたところは、ミイラ取りがミイラになったようでクスッときた。 そこ以外はあわや勝利かいう展開で、熱い一局だった。 この展開って普通、主人公がするんだけどね。

あと、あかりと島田八段、林田先生の3人も良かった。これはもう三角関係ってやつだよな。島田八段は予想通りだったが、まさか林田先生まで参戦するとは。 しかもギャグではなくマジで。 この三角関係はどうなることやら。 島田八段と林田先生、どちらもグイグイくるタイプには思えないので、先が予想できない。 妻子捨男のせいで負ったあかりのトラウマも深そうで、一筋縄ではいかないだろうし。 桐山は結構リアリストだから、この2人だと島田八段を推しそうだけど。 さて、どうなるのか続きが気になる。 あかり視点と林田先生視点の話はあったけど、島田八段視点が無かったので、ぜひとも描いて欲しいところだ。

SQL Server で緯度経度から住所を検索する

緯度経度を使って住所を検索する必要があったが、 SQL Server の地理空間データ型(geography)を使うことで手軽に実現することができたのでメモしておく。

-- 緯度経度と住所を格納するテーブルを作成
CREATE TABLE addresses
(
  id bigint IDENTITY(1,1) NOT NULL,
  address nvarchar(100) NOT NULL,
  location geography NOT NULL,
  CONSTRAINT pk_addresses PRIMARY KEY CLUSTERED 
  (
    id ASC
  )
)
GO

-- geography 型の location にインデックスを作成する
CREATE SPATIAL INDEX spatial_addresses ON addresses
(
  location
)USING  GEOGRAPHY_GRID 
GO

-- テストデータを投入
INSERT INTO addresses
(
  address,
  location
)
VALUES (
  N'福岡県福岡市中央区天神一丁目',
  geography::STGeomFromText(N'POINT(130.401396 33.590878)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  N'福岡県福岡市中央区天神二丁目',
  geography::STGeomFromText(N'POINT(130.398043 33.589390)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  N'福岡県福岡市中央区天神三丁目',
  geography::STGeomFromText(N'POINT(130.396224 33.593515)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  N'福岡県福岡市中央区天神四丁目',
  geography::STGeomFromText(N'POINT(130.399850 33.594580)', 4326)
);

INSERT INTO addresses
(
  address,
  location
)
VALUES
(
  N'福岡県福岡市中央区天神五丁目',
  geography::STGeomFromText(N'POINT(130.398918 33.596781)', 4326)
);
GO

-- 天神二丁目の2の緯度経度に一番近い住所を取得
DECLARE @g GEOGRAPHY;
SET @g=GEOGRAPHY::STGeomFromText('POINT(130.398821 33.589085)', 4326);

SELECT
  TOP(1)
  id,
  address
FROM
  addresses  
WHERE
  location.STDistance(@g) IS NOT NULL  
ORDER BY
  location.STDistance(@g); 

JwtBearerAuthentication を使っている Web API を Swashbuckle.AspNetCore で試す

Swashbuckle.AspNetCore を使うことで、 ASP.NET Core で作成した Web API を Swagger UI 上で試せるようになった。 ただ、大抵の Web API では OAuth2 なり JWT Bearer なりの認証が必要、という風に実装していると思う。 自分の場合は JWT Bearer。

Swagger UI ではそのあたりもサポートしていて、 今回は JwtBearerAuthentication を使っている Web API を Swagger UI 上で試せるか挑戦してみた。

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

namespace SwaggerWithJwtBearer
{
    // アプリケーションの構成
    // 本来は application.json か環境変数に持たせるべきだが、
    // 今回は簡略化のために static クラスにしておく。
    public static class AppConfiguration
    {
        public static string SiteUrl { get; } = "http://swagger-with-jwt-bearer.net";

        // JWT の署名で使う秘密鍵
        public static string SecretKey { get; } = Guid.NewGuid().ToString();
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddAuthentication(options =>
            {
                // JwtBearer 認証しか使わないので、
                // JwtBearer をデフォルトにする
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.Audience = AppConfiguration.SiteUrl;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidIssuer = AppConfiguration.SiteUrl,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(AppConfiguration.SecretKey))
                };
            });

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Title = "Sample API",
                    Version = "v1"
                });

                // Swagger UI で API を試すとき、リクエストヘッダーに
                // JwtBearer トークンを設定できるようにする
                c.AddSecurityDefinition("api_key", new ApiKeyScheme()
                {
                    Description = "Bearer スキームを使用した Authorization ヘッダー. 例: \"Authorization: Bearer {token}\"",
                    Name = "Authorization",
                    In = "header",
                    Type = "apiKey"
                });

                // 設定した Bearer トークンをリクエストに含めるためのフィルタを追加
                c.OperationFilter<SecurityRequirementsOperationFilter>();
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthentication();

            app.UseMvc();

            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "SmartStamp API V1");
            });
        }
    }

    class SecurityRequirementsOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var allowAnonymous = context.MethodInfo
                .CustomAttributes
                .Any(x => x.AttributeType == typeof(AllowAnonymousAttribute));
            if (allowAnonymous)
            {
                return;
            }

            var attributes = context.MethodInfo
                .GetCustomAttributes(true)
                .Concat(context.MethodInfo.DeclaringType.GetCustomAttributes(true))
                .Where(x => x is AuthorizeAttribute);
            if (attributes.Any())
            {
                operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
                operation.Security.Add(new Dictionary<string, IEnumerable<string>>
                {
                    ["api_key"] = new string[0]
                });
            }
        }
    }

    // ユーザー
    public class User
    {
        public string Id { get; set; }

        public string UserName { get; set; }

        public string Password { get; set; }
    }

    // パスワードを除いたユーザー情報を格納する
    public class UserViewModel
    {
        public string Id { get; set; }

        public string UserName { get; set; }
    }

    // トークンを取得するために渡された認証情報を格納する
    public class TokenInputModel
    {
        [Required]
        public string UserName { get; set; }

        [Required]
        public string Password { get; set; }
    }

    // 生成したトークンと有効期限を格納する
    public class TokenViewModel
    {
        public string Token { get; set; }

        public DateTime Expiration { get; set; }
    }

    [Route("api")]
    public class HomeController : Controller
    {
        // テスト用ユーザー
        static List<User> _testUsers = new List<User>()
        {
            new User()
            {
                Id = Guid.NewGuid().ToString(),
                UserName = "tnakamura",
                Password = "test1234",
            }
        };

        // トークンを取得する API
        [HttpPost("token")]
        public IActionResult Token([FromBody]TokenInputModel inputModel)
        {
            if (ModelState.IsValid)
            {
                var user = _testUsers.FirstOrDefault(u => u.UserName == inputModel.UserName);
                if (user != null && user.Password == inputModel.Password)
                {
                    var token = CreateJwtSecurityToken(user);
                    return Ok(new TokenViewModel
                    {
                        Token = new JwtSecurityTokenHandler().WriteToken(token),
                        Expiration = token.ValidTo,
                    });
                }
            }
            return BadRequest();
        }

        JwtSecurityToken CreateJwtSecurityToken(User user)
        {
            // JWT に含めるクレーム
            var claims = new List<Claim>()
            {
                // JwtBeaerAuthentication 用
                new Claim(JwtRegisteredClaimNames.Jti, user.Id),
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                // User.Identity プロパティ用
                new Claim(ClaimTypes.Sid, user.Id),
                new Claim(ClaimTypes.Name, user.UserName),
            };

            var token = new JwtSecurityToken(
                issuer: AppConfiguration.SiteUrl,
                audience: AppConfiguration.SiteUrl,
                claims: claims,
                expires: DateTime.UtcNow.AddDays(7),
                signingCredentials: new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppConfiguration.SecretKey)),
                    SecurityAlgorithms.HmacSha256
                )
            );

            return token;
        }

        // 認証が必要な API
        [Authorize]
        [HttpGet("me")]
        public IActionResult Me()
        {
            var id = User.Claims.First(c => c.Type == ClaimTypes.Sid).Value;
            var user = _testUsers.First(u => u.Id == id);
            return Ok(new UserViewModel
            {
                Id = user.Id,
                UserName = user.UserName,
            });
        }
    }
}

デバッグ実行し、/swagger にアクセスすると Swagger UI が表示される。

f:id:griefworker:20170922113542p:plain

認証必須の API を呼び出すと、401 が返ってくる。

f:id:griefworker:20170922113557p:plain

アクセストークンを取得。

f:id:griefworker:20170922113608p:plain

Swagger UI 上で Authorization ヘッダーの内容を設定できる。

f:id:griefworker:20170922113619p:plain

Authorization ヘッダーにアクセストークンが設定されるようになったので、 認証必須の API を呼び出せるようになった。

f:id:griefworker:20170922113629p:plain

もう Swagger UI の無い Web API 開発には戻れないねぇ。

『Essential Xamarin』を読んだ

Xamarin を扱った技術書もだいぶ増えてきたが、 『Essential Xamarin』はかなり毛色が違う。 それもそのはずで、技術書典で発売された同人誌が元になっていて、 それを加筆修正して商業出版したものが本書。 恥ずかしながら、技術書にも同人誌があるということを知らなかった。 元が同人誌なので、体系的に順序立ててまとまっているわけではない。 WEB+DB PRESSみたいな技術雑誌に近いかな。 収録されている記事がどれも濃かった。

Xamarin.Mac について書かれた記事は初めてお目にかかったかも。 Xamarin で WindowsMac 両対応のアプリの開発を考えていたけど、 正直 macOSアプリ開発をどこから手をつければいいのかさっぱりだった。 本記事ではシンプルなアプリを題材に、 サンプルコードとスクリーンショットを多めに載せてあって、 とっかかりとしてピッタリの記事。 Xamarin.Forms が macOS 対応予定とはいえ、 Xamarin.Mac の知識は必要になるだろうね。

Xamarin の中の人による Xamarin Android SDK の解説記事は、 Xamarin.Android SDK の使い方にとどまらず、 その仕組みにも踏み込んで書いてあった。 Xamarin.Android では Java API を呼び出す C# のラッパーを提供しているわけで、 C# から Java をどうやって呼び出しているのか、 またその逆に Java から C# をどうやって呼び出しているのか、 相互運用について知るのに最適。 Xamarin.Android をやるなら絶対読んでおいた方がいいと思える内容。

とにかく、濃すぎて胸焼けをおこしそうなくらい濃厚な一冊だった。

Essential Xamarin ネイティブからクロスプラットフォームまで モバイル.NETの世界 (技術書典シリーズ(NextPublishing))

Essential Xamarin ネイティブからクロスプラットフォームまで モバイル.NETの世界 (技術書典シリーズ(NextPublishing))