こんにちはHabr! 最新のモバイルアプリケーションインターフェイスには、 トリッキーなグラデーションから株価チャートに至るまで、 多数のイラストやアニメーションが含まれています。 したがって、モバイル開発者は、美しいデザインを機能的なアプリケーションに変えるために膨大な時間を費やす必要があり、さらにさまざまなサイズのデバイスで動作します。
iOSアプリケーションの開発中に遭遇したのは、この問題でした。 タスクを簡素化するために、 Macawグラフィックライブラリを開発しました。これにより、複雑なインターフェイスを明確なシーンオブジェクトの形で記述し、イベントやアニメーションをサポートしてSVGグラフィックを直接表示することもできます。
面白い? この記事では、Macawの基本概念を紹介し、最小限のコードを使用してアニメーションを含む図を作成します。

マコービュー
MacawView
は、Cocoaの世界ですべてのMacawグラフィックを表示するために使用されるメインクラスです。 Macawでの作業を開始するには、独自のクラスを作成し、MacawViewから継承して、内部に必要なインターフェイスを記述する必要があります。 MacawViewはすでにUIView
実装しているため、作成したクラスはCocoaインターフェイスに簡単に統合できます。 これが最も単純な「Hello、World!」です。 コンゴウインコの場合:
class MyView: MacawView { required init?(coder aDecoder: NSCoder) { let text = Text(text: "Hello, World!", place: .move(dx: 145, dy: 100)) super.init(node: text, coder: aDecoder) } }

シーン
Macawは、インターフェイスをテキスト、画像、幾何学的オブジェクトの組み合わせとして説明します。 この組み合わせは、 シーングラフまたは単にシーンと呼ばれます。 シーンの主要な要素を見ていきましょう。
形
形状は、幾何学的形状を表すシーン要素です。 この要素には、3つの主要なプロパティがあります。
- form-図形の形状を決定するドットの幾何学的な場所。 このプロパティを使用して、描画するものを決定します:長方形、円、多角形、または他の何か。
-
fill
-図形内の色 -
stroke
シェイプの周囲の色
最も単純な長方形を見てみましょう。
class MyView: MacawView { required init?(coder aDecoder: NSCoder) { let shape = Shape(form: Rect(x: 100, y: 75, w: 175, h: 30), fill: Color(val: 0xfcc07c), stroke: Stroke(fill: Color(val: 0xff9e4f), width: 2)) super.init(node: shape, coder: aDecoder) } }

Macawは標準のCocoa座標系を使用しているため、上記の例では、iPhone 6 / 6s画面の中央に175x30ドットの長方形(幅は375ドット)を描画しています。 さまざまな画面サイズをサポートするために、いくつかのオプションがあります。
- 固定
MacawView
サイズを使用し、Cocoaの自動レイアウトを使用してMacawView
揃えにします。 - 画面のサイズに応じて、シーンの要素の位置を独立して計算します。 幸いなことに、ベクターグラフィックスはあらゆるサイズに完全に対応します。
コンゴウインコは、他の幾何学的プリミティブもサポートしています。

さらに、MacawにはPath
要素があり、複雑な形状を一連の曲線の形で記述することができます。
例に戻りましょう。 次に、長方形に丸みを付けてみましょう。
let shape = Shape( form: RoundRect( rect: Rect(x: 100, y: 75, w: 175, h: 30), rx: 5, ry: 5), fill: Color(val: 0xfcc07c))

このようなシーンの説明は、宣言的と呼ばれます。 このアプローチでは、シーンをプリミティブツリーに分割して記述します。 コンゴウインコはまた、機能的なスタイルでシーンを記述することができます。 この場合、上記の例は次のようになります。
let shape = Rect(x: 100, y: 75, w: 175, h: 30).round(r: 5).fill(with: Color(val: 0xfcc07c))
どの場合に最適なアプローチを使用するかを決定しますが、コードを読みやすくするアプローチを使用することをお勧めします。
テキスト
シーンの次の基本要素はテキストです。 主な機能は次のとおりです。
-
text
表示するテキスト -
fill
-テキストの色 -
font
-名前とフォントサイズ -
align
/baseline
テキストを整列するためのプロパティ
class MyView: MacawView { required init?(coder aDecoder: NSCoder) { let text = Text(text: "Sample", font: Font(name: "Serif", size: 72), fill: Color.blue) super.init(node: text, coder: aDecoder) } }

お気づきかもしれませんが、 Shape
とは異なり、テキストには位置に関する特別なプロパティがありません。 ただし、各シーン要素にはplace
プロパティがあり、これを使用してシーン要素をその親に対して相対的に配置したり、回転させたりサイズ変更したりすることもできます。 後でこのプロパティに戻りますが、とりあえず次の行を追加しましょう。
text.place = .move(dx: 100, dy: 75)

デフォルトでは、テキストは左上隅を基準にして配置されます。 テキストを中央に配置するには、 align
プロパティを使用できます。
text.place = .move(dx: 375 / 2, dy: 75) text.align = .mid

垂直方向の中央揃えには、 baseline
プロパティも使用できます。
団体
これで、要素の結合に進むことができます。 グループの最も重要なプロパティはcontents
です:グループを構成する要素のリスト:
class MyView: MacawView { required init?(coder aDecoder: NSCoder) { let shape = Shape( form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 3), fill: Color(val: 0xff9e4f), place: .move(dx: 375 / 2, dy: 75)) let text = Text( text: "Show", font: Font(name: "Serif", size: 21), fill: Color.white, align: .mid, baseline: .mid, place: .move(dx: 375 / 2, dy: 75)) let group = Group(contents: [shape, text]) super.init(node: group, coder: aDecoder) } }

要素の中心が原点(0、0)と一致するようにグループ内の各要素を決定し、 .move(dx: 375 / 2, dy: 75)
を使用してこのポイントを画面の中心に転送することに注意してください。 ただし、グループ自体を移動できるようになったため、要素ごとにこれを行う必要はありません。
class MyView: MacawView { required init?(coder aDecoder: NSCoder) { let shape = Shape( form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 5), fill: Color(val: 0xff9e4f)) let text = Text( text: "Show", font: Font(name: "Serif", size: 21), fill: Color.white, align: .mid, baseline: .mid) let group = Group(contents: [shape, text], place: .move(dx: 375 / 2, dy: 75)) super.init(node: group, coder: aDecoder) } }
画像
武器庫の最後の要素は画像です。 次のプロパティがあります。
-
src
ファイルへのパス -
w
/h
画像の実際の高さ/幅 -
xAlign
/yAlign
/aspectRatio
画像をaspectRatio
するためのプロパティ
シーンに画像を追加しましょう:
let image = Image(src: "charts.png", w: 30, place: .move(dx: -55, dy: -15)) let group = Group(contents: [shape, text, image], place: .move(dx: 375 / 2, dy: 75))

デフォルトでは、高さと幅は元の画像の寸法から取得されます。 1つのプロパティのみが定義されている場合、2番目のプロパティは画像の比率に従って自動的に計算されます。
色とグラデーション
上記の例では、色を指定するためのいくつかのオプションをすでに使用しています。
let color1 = Color.blue let color2 = Color(val: 0xfcc07c)
Color
クラスには、他の便利なメソッドがあります。
let color3 = Color.rgb(r: 123, g: 17, b: 199) let color4 = Color.rgba(r: 46, g: 142, b: 17, a: 0.2)
Macawは、線形および放射状のグラデーションもサポートしています。これを使用して、シーン要素のfill
/線のプロパティを設定できます。 各グラデーションは、オフセット付きの色のセットによって定義されます。 勾配の例:
let fill = LinearGradient( // (x1, y1) (x2, y2) // x1: 0, y1: 0, x2: 0, y2: 1, // userSpace true, // , , // (0,0) - // (1,1) - userSpace: false, stops: [ // 0 () 1 () Stop(offset: 0, color: Color(val: 0xfcc07c)), Stop(offset: 1, color: Color(val: 0xfc7600))])
このような勾配の定義は面倒に見えるかもしれませんが、単純な勾配の場合は、より単純なコンストラクターを使用できます。 特に、この例は次のように書き換えることができます。
let fill = LinearGradient(degree: 90, from: Color(val: 0xfcc07c), to: Color(val: 0xfc7600))
コンゴウインコのすべての角度のカウントダウンは時計回りで、3時から始まります。 したがって、90度は上から下への方向にすぎません。

さて、通常の色の代わりに、ボタンをグラデーションで塗りつぶしましょう:
let shape = Shape( form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 5), fill: LinearGradient(degree: 90, from: Color(val: 0xfcc07c), to: Color(val: 0xfc7600)), stroke: Stroke(fill: Color(val: 0xff9e4f), width: 1))

イベント
イベントにより、ユーザーはシーンを操作できます。 コンゴウインコは、 tap
、 rotate
pan
などのイベントを処理できます。 init
メソッドの最後に次の行を追加します。
_ = shape.onTap.subscribe(onNext: { event in text.fill = Color.maroon })
これで、ユーザーがボタンをクリックするとすぐに、色が栗色に変わります。

イベントでは、Macawは非常に強力なRxSwiftライブラリを使用します。 特に、各subscribe
メソッドは特別なDisposable
プロトコルを返します。これにより、登録されたすべてのリスナーを便利に管理できます。 この場合、図が存在する間は常にイベントを処理するため、単に_ =
を使用して表示します。
この例を実行すると、ボタンの中央をクリックしても機能しないことがわかります。 これは、ボタンの形をクリックするとテキストがキャプチャされるためです。 これは、ボタンテキストとその画像に同じハンドラーを追加することで簡単に修正できます。 ただし、より適切な解決策は、これらの要素がイベントを受信できないことを示すことです。 これは、 opaque
プロパティを使用して簡単に実行できます。
let text = Text( text: "Show", font: Font(name: "Serif", size: 21), fill: Color.white, align: .mid, baseline: .mid, place: .move(dx: 15, dy: 0), opaque: false) let image = Image(src: "charts.png", w: 30, place: .move(dx: -40, dy: -15), opaque: false)
変換
すでに見たように、placeプロパティを使用して、シーン上の任意の要素を配置できます。 実際、このプロパティは、ある座標系から別の座標系にポイントを転送できるアフィン変換のマトリックスです。 実際、MacawのTransformクラスは、Core GraphicsパッケージのCGAffineTransformに非常によく似たインターフェースを提供するため、詳細については説明しません。 一般的なプレゼンテーションでは、次のアニメーションで十分です。

チャート
Macawは、グラフやチャートを直接サポートしていません。追加のライブラリがなくても作成が非常に簡単だからです。 まず、これまでに行ってきた変更に順番を付けます。 これが私たちがやったことです:
class MyView: MacawView { required init?(coder aDecoder: NSCoder) { let button = MyView.createButton() super.init(node: Group(contents: [button]), coder: aDecoder) } private static func createButton() -> Group { let shape = Shape( form: Rect(x: -100, y: -15, w: 200, h: 30).round(r: 5), fill: LinearGradient(degree: 90, from: Color(val: 0xfcc07c), to: Color(val: 0xfc7600)), stroke: Stroke(fill: Color(val: 0xff9e4f), width: 1)) let text = Text( text: "Show", font: Font(name: "Serif", size: 21), fill: Color.white, align: .mid, baseline: .mid, place: .move(dx: 15, dy: 0), opaque: false) let image = Image(src: "charts.png", w: 30, place: .move(dx: -40, dy: -15), opaque: false) return Group(contents: [shape, text, image], place: .move(dx: 375 / 2, dy: 75)) } }

次に、ボタンのすぐ下に座標軸を追加します。
required init?(coder aDecoder: NSCoder) { let button = MyView.createButton() let chart = MyView.createChart(button.contents[0]) super.init(node: Group(contents: [button, chart]), coder: aDecoder) } private static func createChart(_ button: Node) -> Group { var items: [Node] = [] for i in 1...6 { let y = 200 - Double(i) * 30.0 items.append(Line(x1: -5, y1: y, x2: 275, y2: y).stroke(fill: Color(val: 0xF0F0F0))) items.append(Text(text: "\(i*30)", align: .max, baseline: .mid, place: .move(dx: -10, dy: y))) } items.append(createBars(button)) items.append(Line(x1: 0, y1: 200, x2: 275, y2: 200).stroke()) items.append(Line(x1: 0, y1: 0, x2: 0, y2: 200).stroke()) return Group(contents: items, place: .move(dx: 50, dy: 200)) } private static func createBars(_ button: Node) -> Group { // return Group() }

次に、ヒストグラム自体を追加します。
static let data: [Double] = [101, 142, 66, 178, 92] static let palette = [0xf08c00, 0xbf1a04, 0xffd505, 0x8fcc16, 0xd1aae3].map { val in Color(val: val)} private static func createBars(_ button: Node) -> Group { var items: [Node] = [] for (i, item) in data.enumerated() { let bar = Shape( form: Rect(x: Double(i) * 50 + 25, y: 0, w: 30, h: item), fill: LinearGradient(degree: 90, from: palette[i], to: palette[i].with(a: 0.3)), place: .move(dx: 0, dy: -data[i])) items.append(bar) } return Group(contents: items, place: .move(dx: 0, dy: 200)) }

実際、 createBars
メソッドcreateBars
は、ソースデータを美しいヒストグラムに変換しました。このために必要なのは、10行未満の明確な宣言型コードです! 今こそ、このチャートを移動するときです。
アニメーション
コンゴウインコの観点から見ると、アニメーションは時間の経過とともにシーンのプロパティを変更するプロセスです。 シーン要素のインターフェイスを注意深く見ると、 opacity
やplace
などのプロパティに加えて、プロパティopacityVar
およびplaceVar
もあることがplaceVar
ます。 これらのプロパティは、アニメーションにのみ使用できます。 たとえば、 opacity
プロパティをアニメーション化するには、 opacityVar
プロパティを使用します。 アニメーションを開始する最も簡単な方法は、 animate
関数を呼び出すことです。
node.opacityVar.animate(to: 0)
この場合、アニメーションはすぐに開始され、 node
要素は完全に消えるまで、1秒以内に徐々に消えます。
アニメーションは、3つの部分のセットとして想像できます。
- アニメーションプロパティ
- アニメーション時間。 デフォルトでは、常に1秒です。
- そして、アニメーションの各ステップの値を生成する関数
Macawでは、この関数を自分で定義できますが、通常は3つの値を使用して定義する方が簡単です。
- from-アニメーションが始まる前に設定される初期値。 設定されていない場合、プロパティの現在の値が使用されます。
- to-最終値
-
easing
-時間に応じて値の変化率を決定する関数
それでは、アニメーションをチャートに追加しましょう。 まず、ヒストグラムのopacity: 0
すべての要素を追加しますopacity: 0
を選択して非表示にし、ボタンをクリックしてアニメーションを開始します。
_ = button.onTap.subscribe(onNext: { _ in bar.opacityVar.animate(to: 1.0) })
アクションの結果:

1行だけで、アプリを動かします! ここで別の効果を試してみましょう:フェードインする代わりに、列がX軸から伸びます。これを行うには、要素をゼロから元の値にスケーリングできます。
let bar = Shape( form: Rect(x: Double(i) * 50 + 25, y: 0, w: 30, h: item), fill: LinearGradient(degree: 90, from: palette[i], to: palette[i].with(a: 0.3)), // y 0 place: .scale(sx: 1, sy: 0)) items.append(bar) _ = button.onTap.subscribe(onNext: { _ in // bar.placeVar.animate(to: .move(dx: 0, dy: -data[i])) })
さらに、さまざまなレイテンシのさまざまな列を表示できます。 これを行うには、 delay
パラメーターを使用します。
bar.placeVar.animate(to: .move(dx: 0, dy: -data[i]), delay: Double(i) * 0.1)
出来上がり! ここで、[表示]ボタンをクリックすると、必要なものが表示されます。

Svg
前述したように、Macawには組み込みのSVGサポートがあります。 SVGParser.parse
メソッドを使用して、他の要素と組み合わせたり、MacawViewに直接渡すことができるシーン要素としてSVGファイルを読み取ることができます。
class SVGTigerView: MacawView { required init?(coder aDecoder: NSCoder) { super.init(node: SVGParser.parse(path: "tiger"), coder: aDecoder) } }

コンゴウインコの基本概念を学習することで、さらに興味深い例を作成できます。 たとえば、数時間で次のようになりました。

プロジェクトの詳細については、 githubページをご覧ください。 ドキュメントと新しいサンプルに積極的に取り組んでいます。アップデートをお待ちください!