Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

API Blueprintで仕様書を作ってみた

これはGalapagos Advent Calendar 22日目の記事です。

こんにちは。ガラパゴスのイバンです。

今回開発している案件ではAPI仕様書の作成を担当しております。 弊社でWeb APIまで実装する場合は大体grape-swaggerのgemで自動生成させますが、 今回はお客さん側で実装されますので仕様書のみが作れるツールがないかを調べてみました。

swaggerで作ることも可能ですが、JSONYAMLを手で書くのはあんまり気が乗らなくて他にないか もうちょっと調べてみました。

理想的には下記の要素が対応しているツールが良い

  • インプットはmarkdown形式
  • アウトプットはhtml形式

同時にモックサーバの検討もしてました。この案件は前からmockable.ioを利用されてましたが、ユーザ数やエンドポイント数の制限があって、他に良いサービスかツールがないかを調べてましたところで、api-mockというnode.jsサーバを見つけました。 残念ながらもうメンテされてない状態ですが、気になったのはAPI仕様書からモックサーバを作るツールであって、その仕様書はAPI Blueprintで書けばapi-mockがそれ読み込んでサーバを立ててくれます。

つまり、API Blueprintで仕様書を書けば、モックまでできてしまう。一石二鳥ですね。

早速API Blueprintのサイトを確認すると嬉しい驚きがありました。

API Blueprint

API BlueprintはAPIの記述ができるMarkdownベースの言語です。

# GET /message
+ Response 200 (text/plain)

        Hello World!

上記のサンプルは/messageをGETで取得すると"Hello World!"がテキストとして返すという仕様になります。

api-mockにblueprintを渡せば、モックサーバが立ち上がって、ブラウザでモックサーバのアドレスを開くとHello World!というテキストが表示されます。

もちろんjsonを返すことを示すのも可能です。

+ Response 200 (application/json)

        [
            {
                "question": "Favourite programming language?",
                "published_at": "2014-11-11T08:40:51.620Z",
                "url": "/questions/1",
                "choices": [
                    {
                        "choice": "Swift",
                        "url": "/questions/1/choices/1",
                        "votes": 2048
                    }, {
                        "choice": "Python",
                        "url": "/questions/1/choices/2",
                        "votes": 1024
                    }, {
                        "choice": "Objective-C",
                        "url": "/questions/1/choices/3",
                        "votes": 512
                    }, {
                        "choice": "Ruby",
                        "url": "/questions/1/choices/4",
                        "votes": 256
                    }
                ]
            }
        ]

上記の例ではjson自体を書きましたが、もう一つの書き方はデータ記述です。その場合はMSONを使ってこうなります。

### List All Questions [GET]
+ Response 200 (application/json)

    + Attributes (array[Question])

## Data Structures

### Question
+ question: Favourite programming language? (required)
+ published_at: `2014-11-11T08:40:51.620Z` (required)
+ url: /questions/1 (required)
+ choices (array[Choice], required)

### Choice
+ choice: Javascript (required)
+ url: /questions/1/choices/1 (required)
+ votes: 2048 (number, required)

jsonはどこへ行った?こっちの方がわかりにくいのでは?ってなるかもしれませんが、説明してないことがあります。A PI Blueprintにはモックサーバ以外、色々なツールがあります。幸いにHTMLレンダラーもありまして、ようやく欲しかったもの全部揃えました。使っているのはAglioというNode.jsライブラリ兼コマンドライン・インターフェースです。

AglioにBlueprintファイルを渡すとhtmlを作成してくれます。しかも、サンプルデータがちゃんとJSONになっていて、しかもJSONスキームも出してくれます。

HTML出力のサンプルはここから見れます

"Show"のリンクを押すと、リクエストとリスポンスのヘッダー、ボディー、スキームが見れます。

素晴らしいですね。

感想

API BlueprintはMarkdownで書かれますのでgithubなどのバージョン管理システムにアップしてチームで編集できてとても良いです。

関連ツールは充実してて簡単にスタブやhtml作成ができちゃいます。

マイナスなポイントは多分、MarkdownなのでBlueprintの規約的にはどこはキーワードか、どこは自分で自由に書けるか、わかりにくいところがあります。

簡単な紹介になりますが、参考になりましたでしょうか。今後も機会あればAPI Blueprintで仕様書を作成したいと思います。

参照

API仕様書フレームワーク

API Blueprintツール

その他

ProtoPieでAppStore動かしてみた!ファイナル 第3回

Galapagos Advent Calendar 企画、21日の記事になります。

adventar.org

こんにちは。UIデザイナーのまあのんです。



ProtoPie動かしてみた!レポートラストファイナル最終回になります。
過去の記事はこちらからどうぞ! ↓

gtech.hatenablog.com

gtech.hatenablog.com


ブログって継続的に続けられた記憶がないのですが、アドベントカレンダーのように前もって日程が決まっているとやらねば!って気もちになります。素晴らしい文化です。




それではやっていきましょう。

あらためて流れを説明すると、

-動かす流れおさらい-

  • スクロールする

  • イメージをタップすると詳細ページに遷移する

  • 詳細ページに遷移する時にイメージが拡がる

  • 一覧に戻る時イメージが縮む


今回で最後まで完成させます!


Step3.イメージをタップして遷移して拡がる


f:id:glpgsinc:20171215192530g:plain:w200
この動きを、、つくりたいのです、、



結論からいうと、レスポンスの動作は下記の形になりました。それぞれ説明していきます。
f:id:glpgsinc:20171221050047p:plain:w700

①グラデーション背景、虹のイラスト
表現したい挙動:ふわっと広がって画面上部に移動
レスポンス:Scale→拡大、Move→画面上部に移動 共にEasingはEase In&Out

第二回ではグループごと動かしてみましたが、それぞれ別レイヤーに変更することで空間に奥行きを感じるようなモーションになりました。
また、Easing機能はEase In&Outをチョイス。他にも多種多様なアニメーションカーブが揃ってます↓
f:id:glpgsinc:20171221060324p:plain:w500

ProtoPie - Basics

公式サイトより。これでも一部です。カスタムできないからなのか、正直AfterEffects先輩もびっくりなラインナップ数。

マテリアルデザインのモーションデザインの原則によると、現実世界のような普段から慣れ親しんだ動きを取り入れることで、使う人が心地よく理解しやすいモーションになると考えられています。動きをつける際は一度マテリアルガイドラインに目を通しておくことをおすすめします!

Material motion - Motion - Material Design


②今日のAPP/値段/アプリ名テキスト、アプリアイコン
表現したい挙動:画面上部に移動
レスポンス:Move→画面上部に移動

③タブバー/ステータスバー
表現したい挙動:すっと消える
レスポンス:Opacity→100%から0%に変更

④テキストエリア
表現したい挙動:画像下からすっと出てきて、テキストが遅れて表示
レスポンス:Opacity→0%から100%に変更
テキストはタイムライン上でかなり後に設定しています。


一通りの動きをつけ終えました。
ここでプレビューを見てみましょう。

f:id:glpgsinc:20171221103209g:plain:w200


ヌルヌル!!!!







f:id:glpgsinc:20171221062210p:plain:w500


忘れてはいけない大切なレスポンス、Jump。


■遷移系のインタラクションでかかせない『Jump』

Jumpはシーン間の移動ができます。このJump以外のレスポンスで十分遷移されたように見せることは出来ますが、レイヤー構造が複雑で遷移先のアクションも必要な場合は一度Jumpでシーン移動することをおすすめします。ちなみにシーン移動する際はフェードインなど簡単なTransitionを追加することができます。

今回は遷移時に複雑なアクションが必要だったため、一旦このシーンで遷移後の画面を作成→それと同じ画面のシーンをJumpで差し換えてアクションを行います。




Step4.詳細画面でのアクション→一覧に戻る時にイメージが縮む



Jumpでシーン移動した後のアクションになります。 f:id:glpgsinc:20171221071536p:plain:w700

詳細画面に表示されるポップアップの動きもさくっと再現していきます。↓

f:id:glpgsinc:20171221111346g:plain

①アプリのポップアップ
表現したい挙動:スクロールして上部イメージが見えなくなると下からぽよんと現れる
トリガー:Range 対象→スクロール範囲 Property→スクロール position450(テキストエリアあたり) ≦ ScrollOffsets of スクロール
レスポンス:Move ×2→下から定位置より少し上に移動/定位置に移動 共にEasingはEase In&Out

■条件がそろうと発生するアクション、『Range』
レイヤーのプロパティが変わる際、ユーザーが設定した条件を満たした場合にレスポンスが行われます。
ポップアップの場合、スクロール内の特定の範囲のみ表示する仕組みだったため、対象をスクロールコンテナ、条件をイメージとテキストエリアの間辺りから下へスクロールすると発生する形にしました。


レスポンスではMoveを2回。軽くバウンドするような動きをつけるためタイムラインをずらしています。
ぽよんぽよんって動き、、ほんとは動かしてる時めっっちゃくちゃ楽しいから色々遊びたくなる。。けど、今回はあくまで模写修行です。我慢我慢


②アプリのポップアップ
表現したい挙動:表示されてる状態でイメージのエリアまでスクロールするとスッと消える
トリガー:Range 対象→スクロール範囲 Property→スクロール  ScrollOffsets of スクロール ≦ position260(イメージエリアあたり)
レスポンス:Opacity→100%から0%に変更


バツボタン
表現したい挙動:バツボタンをタップすると一覧画面に遷移する
トリガー:Tap 対象→バツボタン
レスポンス:Jump→シーンを最後のアクション追加用の画面に変更

最後はやっぱりJump追加です。このシーンではjumpで遷移後に縮むモーションを追加していきます。


最後のシーンに遷移してきました! f:id:glpgsinc:20171221082151p:plain:w700

ここでは詳細から一覧の画面に戻るだけなので、レスポンス的にはお馴染みのもので逆戻りしているだけです。
トリガーのStartをご説明します。
f:id:glpgsinc:20171221082920p:plain:w500

■シーン移動後自動的にうごきだす『Start』
プロトタイプが実行・またはシーンが展開された際にレスポンスが展開されます。Twitterのようなアニメーションのスプラッシュも作れちゃいます!

これをトリガーに縮むモーションを追加して、完成です!




完成、、、、

完成、、、、、、、、、、、、




ぜひ実機で、さわってみてください。↓

share.protopie.io



とはいえ、プロトタイプ



とはいえ、プロトタイプです。表現の幅に限度があるようです。

実際に触ってみるとわかるのですが、一覧画面で初期位置からズラしてタップすると、広がるはずのイメージ画像が画面の上部固定で表示されず、強制的に詳細画面に遷移するような挙動になります。ムムム。
どうやらスクロール可能なグループから複雑な遷移のアクションは指定が難しいようで、公式で配布されてるデータでは複雑な遷移のものは決まって画面固定でした。
(ひとつだけ、pagingメインでタップすると画面内の指定の場所に配置されるギャラリーを発見したものの、解読に断念)
これに苦戦しているようならいっそXcodeを体得したほうがいいのではとも、、
このこがどこまで出来て、何を苦手としているか、これからもっと探求していきます!


もし解決方法しってるよ!とがいらっしゃいましたらお気軽にメッセージお願いしますm(._.)m

とはいえ、便利



とはいえ便利です。実際にクライアントへデザインをプレゼンする際にProtoPieをつかってみたところ、
いつものプロトタイプと比べてヌルヌルしてるじゃん!すごいじゃん!といつもよりテンションが1段階あがっていました。
(提案物に対しての反応ではないので、素直に喜んでいいのか分からなかったけど)

これまでは静止画のプロトタイプ、AfterEffectsで作成した動画を流してプレゼンしていましたが、パット見のビジュアルで判断され、結果的にユーザーの使い勝手を損なう危険をはらんでいました。

思い描いた動きを短時間で作成でき、かつ実機で直接操作感をチェックできるProtopieは
デザインに大きな説得力を持たせてくれる、UIデザイナーにとって理想の具現化ツールなのかもしれません!


以上、Protopieでつくってみたレポートこれにて終了です。

adventar.org

Galapagos Advent Calendarも、のこり2記事となりました。最後までお見逃しなくです。


参考記事
https://www.protopie.io/learn/
さよなら Pixate, よろしくProtoPie – heru – Medium
【Protopieのススメ】UIデザイナーのためのインタラクションモックアップツール - Qiita

CoreMLでリアルタイムなスタイル変換

これはGalapagos Advent Calendar 20日目の記事です。

二度目まして。iOSチームの高橋です。好きな金額は二兆円です。

今回はiOS上で簡単にニューラルネットのモデルを実行させられるCoreMLを利用して、リアルタイムなスタイル変換を実装する話をします。

準備

Kerasモデルファイルの入手

さて、リアルタイムなスタイル変換を行う手法としてはarXiv:1603.08155が存在しますが、なんと!昨日の記事でまんださんがこれをKerasで実装してくれています!(しらじら)

なので、できあがったh5モデルファイルをもらうことにしました。これさえあればモネ風のスタイル変換ができる、はずです。

mlmodelへの変換

Kerasのモデルファイルをもらったので、coremltoolsを使ってmlmodelファイルに変換します。 coremltoolsは現時点ではPython2系にしか対応していないようなので、しぶしぶPython2を使います。

$ pip install -U coremltools
>>> import coremltools
WARNING:root:Keras version 2.1.2 detected. Last version known to be fully compatible of Keras is 2.0.4 .
WARNING:root:TensorFlow version 1.4.1 detected. Last version known to be fully compatible is 1.1.1 .
>>> coreml_model = coremltools.converters.keras.convert('./monet2.h5', input_names='input_1', image_input_names='input_1', output_names='transform_output')

(中略)

ValueError: Unknown layer: InputNormalize

はい、失敗しましたね。今日のblogはここまでです。ありがとうございました。

……というわけにはゆかないので、今回はこれを乗り越えてみせたいと思います。

なぜなのか

なぜ失敗するのかというと、元になったKerasのモデルがKerasにはない独自のレイヤーを定義して使用しているからです1。 するとcoremltoolsは「そんなレイヤー知りません」という顔でエラーを投げて寄越すわけです。

# たとえばこういうレイヤーがあると変換に失敗する
class InputNormalize(Layer):
    def __init__(self, **kwargs):
        super(InputNormalize, self).__init__(**kwargs)

    def build(self, input_shape):
        pass

    def compute_output_shape(self,input_shape):
        return input_shape

    def call(self, x, mask=None):
        return x/255.

いまさら純Kerasで作り直してもらうわけにもゆかないので、これは困りました。

tf-coreml

ところで話は変わりますが、TensorFlowのモデルからmlmodelへ変換するスクリプトが実は存在します(いくらかの制限つきではありますが)。

github.com

ということは、KerasのモデルをTensorFlowのモデルに変換することができれば、この問題を乗り越えることができるかもしれません。 そしてKerasのモデルがTensorFlowをバックエンドとして利用しているならば、それは原理的には可能なはずです。

Keras to TensorFlow

そう思って検索をかけてみると、先行研究を発見することができます。 だよね〜、と思いながらモデルファイルを投入します。

$ python keras_to_tensorflow.py -input_model_file ./monet2.h5 -output_model_file ./monet2.pb
('input args: ', Namespace(f=None, graph_def=False, input_fld='.', input_model_file='../monet2.h5', num_outputs=1, output_fld='.', output_graphdef_file='model.ascii', output_model_file='../monet2.pb', output_node_prefix='output_node'))

(中略)

ValueError: Unknown layer: InputNormalize

コケてしまいました。やはり「そんなレイヤー知りません」ということのようです。えっじゃあKerasは独自に定義したレイヤーを含むモデルは配布できないってことですか?と思って検索すると、こういうコメントを発見しました:

model = keras.models.load_model('temp_model.h5',
              custom_objects={'Melspectrogram':kapre.time_frequency.Melspectrogram})

なるほど、load_model時にレイヤークラスを渡してやればいいようです。なのでkeras_to_tensorflow.pyをすこし修正して再挑戦。

net_model = load_model(weight_file_path, custom_objects={'InputNormalize': InputNormalize,
                                                         'ReflectionPadding2D': ReflectionPadding2D,
                                                         'Denormalize': Denormalize,
                                                         'VGGNormalize': VGGNormalize,
                                                         'dummy_loss': dummy_loss})
Converted 122 variables to const ops.
('saved the freezed graph (ready for inference) at: ', '././monet2.pb')

やった!これでTensorFlowのモデルファイルが手に入りました。

TensorFlow to CoreML

手に入ったTensorFlowモデルをtf-coremlに投入してやります。

>>> import tfcoreml
WARNING:root:Keras version 2.1.2 detected. Last version known to be fully compatible of Keras is 2.0.6 .
WARNING:root:TensorFlow version 1.4.1 detected. Last version known to be fully compatible is 1.2.1 .
>>> coreml_model = tfcoreml.convert(tf_model_path='monet2.pb', mlmodel_path='monet2.mlmodel', input_name_shape_dict={'input_1:0': [1, 256, 256, 3]}, output_feature_names=['transform_output/mul:0'], image_input_names=['input_1:0'])

(中略)

 Core ML model generated. Saved at location: monet2.mlmodel

Core ML input(s):
 [name: "input_1__0"
type {
  imageType {
    width: 256
    height: 256
    colorSpace: RGB
  }
}
]
Core ML output(s):
 [name: "transform_output__mul__0"
type {
  multiArrayType {
    shape: 3
    shape: 256
    shape: 256
    dataType: DOUBLE
  }
}
]

これでmlmodelファイルが手に入った……ように見えますが、よく見ると出力の型がmultiArrayTypeになっています。これだと画像として出力されないので、Swift側で変換コードを書いてやる必要があるのですが、それはイケてないなあと思っていたところ、AppleのDeveloper Forumに解決方法が書かれていました。

forums.developer.apple.com

こういう関数を定義して噛ませればよいようです:

def convert_multiarray_output_to_image(spec, feature_name, is_bgr=False):
    """
    Convert an output multiarray to be represented as an image
    This will modify the Model_pb spec passed in.
    Example:
        model = coremltools.models.MLModel('MyNeuralNetwork.mlmodel')
        spec = model.get_spec()
        convert_multiarray_output_to_image(spec,'imageOutput',is_bgr=False)
        newModel = coremltools.models.MLModel(spec)
        newModel.save('MyNeuralNetworkWithImageOutput.mlmodel')
    Parameters
    ----------
    spec: Model_pb
        The specification containing the output feature to convert
    feature_name: str
        The name of the multiarray output feature you want to convert
    is_bgr: boolean
        If multiarray has 3 channels, set to True for RGB pixel order or false for BGR
    """
    for output in spec.description.output:
        if output.name != feature_name:
            continue
        if output.type.WhichOneof('Type') != 'multiArrayType':
            raise ValueError("%s is not a multiarray type" % output.name)
        array_shape = tuple(output.type.multiArrayType.shape)
        channels, height, width = array_shape
        from coremltools.proto import FeatureTypes_pb2 as ft
        if channels == 1:
            output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('GRAYSCALE')
        elif channels == 3:
            if is_bgr:
                output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('BGR')
            else:
                output.type.imageType.colorSpace = ft.ImageFeatureType.ColorSpace.Value('RGB')
        else:
            raise ValueError("Channel Value %d not supported for image inputs" % channels)
        output.type.imageType.width = width
        output.type.imageType.height = height

実際に試してみます。

>>> spec = coreml_model.get_spec()
>>> convert_multiarray_output_to_image(spec, 'transform_output__mul__0')
>>> newModel = coremltools.models.MLModel(spec)
>>> newModel.save('monet2.mlmodel')

これで……

f:id:glpgsinc:20171218155353p:plain

できました!!!

あとはこのmlmodelファイルをスッとXcodeに投入すれば、画像の変換を行うクラスが生成されます。長かった…… 便利な世の中ですね。

実装

メインロジック

実装のメイン部分はCoreMLのおかげで非常に簡潔です。こんな風に:

import AVFoundation
import CoreImage
import UIKit

class ViewController: UIViewController {

    @IBOutlet private weak var imageView: UIImageView!

    let model = monet2()
    let imageSize = CGSize(width: 256, height: 256)

    /// メインロジック。CVPixelBufferを受け取って変換して表示する
    ///
    private func pixelBufferDidUpdate(pixelBuffer: CVPixelBuffer) {
        let output = try! model.prediction(input_1__0: pixelBuffer)
        let image = UIImage(ciImage: CIImage(cvPixelBuffer: output.transform_output__mul__0))
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }

    let session: AVCaptureSession = AVCaptureSession()
    let videoQueue: DispatchQueue = DispatchQueue(label: "videoqueue")
    var pixelBuffer: CVPixelBuffer? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        heavyLifting()
    }

}

で、残る問題は動画のキャプチャとピクセルバッファのクロップなのですが、それについては以下で簡単に説明したいと思います。

AVFoundationによる動画のキャプチャ

今回はリアルタイムなスタイル変換を行いたいので、カメラからの入力をリアルタイムに受け取る必要があります。 こういう場合にはAVFoundationを使えばよいらしいので、そのためのセットアップを行います2

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func heavyLifting() {
        CVPixelBufferCreate(kCFAllocatorDefault,
                            Int(imageSize.width), Int(imageSize.height),
                            kCVPixelFormatType_32BGRA, nil, &pixelBuffer)

        let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)!
        device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 30)

        let videoInput = try! AVCaptureDeviceInput(device: device)
        session.addInput(videoInput)

        session.sessionPreset = .hd1280x720

        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
        videoDataOutput.setSampleBufferDelegate(self, queue: videoQueue)
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        session.addOutput(videoDataOutput)

        let connection = videoDataOutput.connection(with: .video)
        connection?.videoOrientation = .portrait

        session.startRunning()
    }

冒頭のCVPixelBufferCreateは、CoreMLとのやりとりに使うピクセルバッファを作成するためのもので、AVFoundationとは関係ありません。 その下では背面カメラから動画の入力を受ける設定をしており、動画サイズは1280×720だという指定が続きます。最後に出力の受け先をselfに向けて撮影を開始しています。

CoreImageによるフレームの切り抜き

あとはキャプチャしたフレームを適切な大きさにクロップしてCVPixelBufferに書き込む必要がありますが、これはCoreImageを経由することで実現できるようです。筆者はCoreImageに明るくないのでけっこう苦労しましたが、Stack Overflowに助けられてなんとか画像の中央を切り出すことに成功しました。

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        let ciImage = CIImage(cvImageBuffer: CMSampleBufferGetImageBuffer(sampleBuffer)!)
        let originalRect = ciImage.extent
        let cropRect = CGRect(x: (originalRect.width - imageSize.width) / 2,
                              y: (originalRect.height - imageSize.height) / 2,
                              width: imageSize.width, height: imageSize.height)
        let cropped = ciImage.cropped(to: cropRect)
        let transformed = cropped.transformed(by: CGAffineTransform(translationX: -cropRect.origin.x, 
                                                                    y: -cropRect.origin.y))
        let context = CIContext()
        context.render(transformed, to: pixelBuffer!)
        pixelBufferDidUpdate(pixelBuffer: pixelBuffer!)
    }

}

こうしてできたpixelBufferを上で書いたメインロジックに渡してやれば、CoreMLの力で画像が変換されるので、CIImageを経由してUIImageにしてUIImageViewに表示してやれば完成です。

結果

弊社エントランスにかざってあるクリスマスツリーをモネ風にしてみました。端末はiPhone Xを使用していますが、iPhone 7でも似たような速度で動きます。

f:id:glpgsinc:20171218185207g:plain

いかがでしょうか。モネ風かどうかはともかく、なにかしらスタイルが変換されていることがおわかりいただけるかと思います3。 関係ないですが、端末がまあまあ熱くなるので、無茶をしているのだなあという気持ちになります。

おわりに

ということで、iOS端末上でリアルタイムにスタイル変換をするアプリを簡単に作ることができました。次はこっちの論文なんかも実装してみたら楽しいかもしれませんね。

ところで

弊社では機械学習に興味があったりなかったりするエンジニアを募集しています。ご応募お待ちしております。

www.glpgs.com

以上です。明日はUIデザイナーのまあのんさんがProtoPieについて書いてくれるそうですね。お楽しみに。


  1. という記事を書いている間に判明したのですが、iOS 11.2以降は独自のレイヤーに対応させることが(がんばれば)可能なようですね(参考)。

  2. あっ、Info.plistのNSCameraUsageDescriptionを設定しておくのを忘れないでくださいね。

  3. もっとも、元からわりと派手なクリスマスツリーだという話はあるのですが。