くらしのマーケット開発ブログ

くらしのマーケットを開発する、みんなのマーケットによる技術ブログ

スクラムを導入してみた話

はじめに

こんにちは、ディレクターのツカモトです。会社にはネコが2匹いますが、ネコの手の色でしか違いがわからないので心の中では「シロ」「シロじゃないほう」と呼んでいます。(名前はちゃんとあります)

みんなのマーケットの一部の開発チームでは2ヶ月ほど前からスクラムを導入して開発を進めています。全員スクラム未経験からのスタートでまだまだ課題も山積みですが、良かったこともたくさんあるので今日は少し紹介したいと思います。 スクラム自体の説明は割愛しますが、「スクラムってなに?」という方はスクラムガイドを読んでみてください。 https://www.scrumguides.org/docs/scrumguide/v2017/2017-Scrum-Guide-Japanese.pdf

スクラムを導入した経緯

スクラムを導入する前の開発部門は3~4名を1チームとし、3チーム体制で基本的にメンバー固定で開発を進めていました。メンバー固定で開発をしていると、特定メンバーとのコミュニケーションは密になる反面、チーム・個人単位で知識やスキルの偏りが時間の経過とともに大きくなっていました。そしてその偏りが、開発するうえでの制約となり生産性や品質に影響を及ぼすようになってきました。また、「チームとは何なのか?」と悩んでいた時でもありました。 そんな時、「カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで」という本に出会ったこと、またこの本を他のチームのエンジニアの、のりすけが読んでくれたこともあり、わりと唐突に2チームを合体して1チームとし、PO2名エンジニア5名体制でのスクラムが始まりました。

良かったこと

f:id:curama-tech:20180518103011p:plain

経験者がひとりもいない中、本やネットでの情報を頼りに始めたスクラムですが良かったと思う点は多くありました。

  • ふりかえりの習慣ができた! -> 今は1スプリントを1週間としているので、毎週KPTでふりかえりを行なっています。スクラムを開始する前と比較するとProblemを共通認識するようになり、またProblemをチーム全体でTryに落とし込むため様々な解決案が出されるようになり解決済となったProblemは確実に増えました。
  • コミュニケーションがとても増えた! -> スクラムを始めて数週間は、多くのメンバーがふりかえりのKeepとしてコミュニケーション増加をあげていました。
  • ペアプロやモブプロが盛んに行われるようになった! -> これまでペアプロはたま〜に行われる程度でしたが、今では毎日一定の時間行われています。すごい変化!目指せ属人化排除!
  • 情報の透明化! -> タスクは今までGithubで管理していましたが、テックメンバーだけでなく全員みれるようTrelloでの管理に変更しました。タスクに関するやりとりは基本的にTrelloに集約し、「なんでxxなんだっけ?」を引き出しやすくしています。

上記に加え、エンジニアからはこんな声も寄せられました。

  • コミュニケーションが密になった。ペアプロ・モブプロをちゃんとやるようにした事によって知識が共有できたことに加えて、レビューの効率がかなり良くなった!
  • 同じ言葉で会話ができるケースが増えて、職種関わらず同じ言葉を使って話をできるようになった!
  • デプロイ前のめちゃ不安な状況をかなり軽減できてすごく嬉しい!

課題

良かった点もたくさんありますが、「スクラムは、軽量・理解が容易・習得は困難」といわれているように、習得というにはまだまだといったところです。また、当然ではありますがスクラムのデメリットにもぶつかっています。

  • 知識やスキルの偏りが大きかったためプランニングに時間がかかる&生産性が一時的に落ちている -> 一時的な話のはずなので徐々に改善すると見込んでます。今後に期待!
  • ベロシティが定まらない -> スプリントによってかなりバラつきがある。プランニングポーカーでストーリーポイントが一定数以上の場合はストーリーを分割するなどを試し中。
  • 1スプリントを1週間としているが、あっという間すぎる。Doingのまま次のスプリントを迎えてしまうことも少なくない。 -> それぞれメリットデメリットはあると思うが、近々2週間でも試してみようと思う。

さいごに

スクラムを始める前と比べてエンジニアとのコミュニケーション時間が圧倒的に増えましたが、この人達すごいなぁ〜と思うことがよくあります。そんな学びの多い人たちと一緒にプロダクトを作れることをとても嬉しく思っています。

f:id:curama-tech:20180518102921j:plain

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

次回は、めぐみちゃんがお届けします!

SVGスプライトの実装方法

はじめに

みんなのマーケットでデザイナーをしているミソサクです。

オフィスには猫が2匹います。ジャンゴ(♂)と、シー(♀)です。私のデスクにはシーちゃんがよく来ます。みんなのマーケットに入社時から2年半くらいコツコツおやつをあげたり「ケツトントン」をしてやっと友達として認められました。とっても可愛いです。だけどズルい女。

仰向けで寝るシーちゃん

先日SVGスプライトを使ったので、実装方法をざっくり書きます。

SVGスプライトとは

アイコンやロゴなど、複数のSVGデータを1つのファイルにまとめて、使いたいデータだけ呼び出す方法です。

なんで今SVGスプライト?

ずばり、スマホページの表示速度改善のためです!「写真」としての役割の画像はいいとして、「アイコン」や「あしらい」としての役割の画像について、くらしのマーケットでは形式がバラバラでした。

  1. jpg
  2. gif
  3. png
  4. svg
  5. 外部Fontファイル
  6. オリジナルFontファイル

そして20回も30回もサーバーにリクエストをしています。

最近追加したアイコンは、IcoMoonを使ってオリジナルFontファイル(上記6)1つにまとまってたんですが、ネットワークが遅いと一瞬文字化けするのが個人的に許せませんでした。ということで、最初に用意する工数は増えるけど最速でアイコンを表示できそうなSVGスプライトにしました。

達成したいこと

全ての画像(写真を除く)において

  • リクエスト数を1にする(表示速度改善)
  • cssで拡大縮小可能にする(ベクターデータにしてRetina対応)
  • cssで色指定できるようにする(色変更のために作り直したくない)

仰向けでこっちを見るシーちゃん

はい、では実装

実装

(1)SVGの準備

サイト上のすべてのアイコンを洗い出します。cssだけで表現できる画像は削除してコード化します。イラレで必要なアイコンを書いて、SVGで保存。軽量化が目的なので、書き出す時は「Illustratorの編集機能を保持」のチェックは外します。

くらしのマーケットロゴデータ

(2)SVGを一つのファイルにまとめる

まとめ役のファイルall-icons.svgの記述

<svg display="none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
        // ここに各SVGコードを書く
    </defs>
</svg>

<defs></defs>の中身、各SVGコードの記述

<symbol id="logo" viewBox="0 0 24 24">
    <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</symbol>

お察しの通りidは呼び出す時に使います。fill属性の記述は削除してcssでの色指定を可能にします。

(3)表示する

召喚の呪文

<svg class="icon-logo">
    <title>くらしのマーケット</title>
    <desc>くらしのマーケットのロゴです。</desc>
    <use xlink:href=“/path/to/all-icons.svg#logo"/>
</svg>

#の後に呼びたいデータのid名を書きます。

(4)cssで色やサイズを指定

.icon-logo {
    fill: #fff;
    height: 24px;
    width: 24px;
}

色の指定はcolor ではなくて、fillです。

基本的な使用方法は以上ですが、アイツが黙ってるわけない。

IE11問題

IE11はSVGの表示はできるけど、use要素の表示には対応してないらしいです。svg4everybodyという便利なjsを用意してくれている英雄がいたので使わせて頂きました。

<script src="/path/to/svg4everybody.js"></script>
<script>svg4everybody();</script>

まさかのiOS 9.3問題

ここまで実装してなんとiOS 9.3で、アイコンを表示する度にall-icons.svgを毎回毎回サーバーまで呼び出している事がわかりました。キャッシュしてくれない。リクエスト数1回にするどころか、爆発的に増やしている。。。。。。。。調べても対応策は見当たらない。。。。うーん。。。。。。。。

パソコンに乗るシーちゃん
いくら可愛くても、そこは座っちゃダメ。

最終手段

そもそもSVGはコードなんだから、all-icons.svgの中身をhtmlに直接書いてしまおう! うちはテンプレートを使っているので書くのも一箇所でOK!これによって、htmlは長くなるけど、アイコン系の画像のリクエスト数は0になりました!しかもuseの記述も短くなった!

// before
<use xlink:href=“/path/to/all-icons.svg#logo"/>

// after
<use xlink:href=“#logo"/>

めでたし!めでたし!

結果

全ての画像(写真を除く)において

  • リクエスト数が0になりました!
  • cssで拡大縮小可能になりました!
  • cssで色指定できるようになりました!

以上、ざっくりですがSVGスプライトの実装方法でした!

眠るシーちゃん

最後に

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

次回は、オフィスにいる猫のオスとメスの区別がつかない、ディレクターのツカモトさんがスクラムについて書きます。

新卒でエンジニアとし入社して1ヶ月働いてみて感じたこと

新卒でエンジニアとし入社して1ヶ月働いてみて感じたこと

テックチームの都築です。
この記事では、私がみんなのマーケットに新卒のエンジニアとして入社して見て考えたことや感じたことを書いていきたいと思います。

以下が今回のブログの目次となっています。

目次

  • この会社に新卒でエンジニアとして入社してみて良かったこと悪かったこと
  • 私と同期の人たちとの関係について
  • 私の両隣にいる人たちについて
  • 私の担当している仕事について
  • 1ヶ月働いてみて感じたこと
  • これからの目標

この会社に新卒エンジニアとして入社してみて良かったこと悪かったこと

 まずは、この会社に入社してみて良かったことについて書いていきたいと思います。
 1つ目は、最初に仕事用のMacを貸してもらえる点です。私は今、MacBook Pro 2017 13インチモデルを使用して仕事をしています。仕事でプログラムを作成するときと個人的にプログラムを作成するときの環境を物理的に変更できるおかげで、心置き無く開発が進められます。
 2つ目は、新卒で右も左も分からない私に、Webページやアプリのテストを任せてもらっている点です。会社のシステムを理解するためにはテストをするのが一番とのことらしいのですが、とても重要な最終防衛ライン的部分を任せてもらえていることはとても誇りに思っています。
 3つ目は、駄菓子をいつでも食べられる点です。ちょっと方向性に行き詰まった時や、ちょっとした休憩のお供には最高ですが、食べ過ぎには注意しないといけないと思っています。
 4つ目は、新鮮な水がいつでも飲める点です。人間が生きるために必須な水がいつでも冷たい状態で飲むことができることはとてもありがたいことだと思っています。以下の写真が私の生命を支えている水です。
 新鮮な水

 次に、この会社に入社してみて悪かったことについて書いていきたいと思います。
 1つ目は、Webページやアプリのテストを任せてもらっている点です。嬉しいことでもありますが、普通に大変です。(楽しいから別にいいかなと思ってます)
 2つ目は、新卒入社のエンジニアが私だけという点です。寂しいです。(周りの同期が優しいから最近はあまり寂しさを感じないようになりました)
 3つ目は、悪い点をあげていると特に思い浮かばずに他の人に「え、それ本当に思ってる」と言われる点です。(とりあえず今のところは本心です)  

私と同期の人たちとの関係について

 同期の人たちの優しさが身にしみます。おかげで今週も頑張れます。
 しかし、具体的に優しさってなんだいと言われそうなんで細かく書きます。
 1つ目は、エンジニアとしての仕事をできるだけ理解しようとしてくれている点です。エンジニアの仕事と言えば「何やってるか分からない」とか言われるという話を大学時代の友人から聞きます。しかし、同期の人たちはそんな私のわけわからない話を理解しようとしてくれる優しさがとても嬉しいです。
 2つ目は、部署関係なくみんなの仲がいい点です。(私はそう思っています)よく飲みにいきながらどういう仕事をしているなどの情報交換ができるのでとてもありがたいですし、とてもいい刺激になります。  

私の両隣にいる人たちについて

 私が仕事をしている席の両隣にはベトナム出身のズイさんトゥエンさんがいます。お二人は普通に日本語が喋れます。たまに、これはどういう意味などの日本語に関することについて聞いてきてくれるのが嬉しいです。日本人に生まれてよかったーと思います(大げさ)最近は、少しだけベトナム語も教えてもらっています。
 少し話が脱線しましたが、お二人は私にとってとてもいい刺激をくれる存在です。日本語を頑張って覚えようとするお二人を見ていると「頑張って仕事します!」と思えます。さらに、お二人にプログラムのことなどについて質問すると、とてもわかりやすく教えてくれます。技術力の高さに救われてます。cảm ơn bạn(ベトナム語でありがとうと言う意味だそうです)  

私が担当している仕事について

 私は、WebのUIなどが正常に動くか試すためのテストを行うプログラムを書いています。利用している言語としてPythonテストフレームワークとしてrobotframeworkを利用しています。また、シミュレータの制御にはAppium、WebテストのライブラリとしてSeleniumを利用しています。
 では、突然ですが、少しrobotframeworkの書き方について説明したいと思います。
 
 まずは、プログラムで何かしら表示する操作から説明していきます。
 

log to console  hello word

 hello wordと表示するということです。  ここで注目するべき点は、log to consolehello wordの間の空白は2つですが、logtoconsoleの間の空白は1つという点です。
 
 続いて、変数に値を入れるという操作から説明していきます。
 
${minma}=  set variable  tech

 わかる方にはわかると思いますが、${minma}という箱のなかにtechという文字を入れておくということです。
 ここで注目すべき点は、変数を定義するときは、${変数名}という形を取らなくてはいけないということです。
 
 続いては、条件式です。
 
run keyword if  ${minma} > 0  log to console  くらしのマーケット

 ${minma}が0より大きければくらしのマーケットと表示するということです。
 ここで注目するべき点は、if文が長い点です。他にも色々な種類のif文があるので調べてみると面白いかもしれません。
 
 と言った感じでプログラムを記述しています。
 もっとrobotframeworkについて知りたいと思った方はこちらを参考にして見てください。  

1ヶ月働いてみて感じたこと

 結論から言うと、ここの会社で働けてとても嬉しいです。
 一緒に働いている方たちもとても優しくミスをしても次頑張ろうと言ってくださる優しい方達です。(たまに本気で怒られる時もありますが。。。)
 仕事もやっていて楽しいです。
 職場の環境にも満足しています。(職場環境が良すぎて一部、自制心を持たないといけない場合があるので注意しています)  

これからの目標

 ここからは1ヶ月働いて見て感じたこと思ったことから目標を設定していきたいと思います。
 1. テスト以外にも様々なプログラムを作れるようになりたいと思っています。
 2. techだけでなく他の部署のことも知りたいと思うようになりました。なので、色々な部署の仕事について学んでいきたいと思っています。
 3. 新卒だけでもいいので部署間交流を活発にやっていきたいと思っています。(新卒だけで色々やっているよねと言う内側からの声は聞こえないことにします)

いかがでしたか?私個人の独断と偏見によりこの記事を書いているので他の方々がどう思っているかはわかりません。しかし、これだけは自信を持って言えます。みんなのマーケット株式会社に入社できてよかった。

一緒に働いてみたいといった方もぜひお待ちしてます!

凸最適化問題の紹介

はじめに

テックチームのトゥエンです。

この記事では、機械学習や他の多くの分野に適用される興味深い部分を紹介します。 それは凸最適化問題です。

猫

問題

めぐみさんは、ワインが大好きです。ワインが大好きすぎて自分でワインを作ることにしました。 めぐみさんは、ワインを作るためのフルーツとして葡萄と苺を育て、ワインにすることにしました。 しかし、安く早くワインが飲みたいめぐみさんは予算は1万8千円、期間は1ヶ月(30日)でワインを作りたいと思っています。また、 めぐみさんは畑を所有しており、その畑の面積は800平方メートルあります。葡萄栽培のコストは3千円、苺は千円です。葡萄の栽培時間は、100平方メートルあたり2日間、苺の栽培時間は100平方メートルあたり5日間です。100平方メートルあたり葡萄は50リットルのワイン、100平方メートルあたり苺は30リットルのワインを作ることができます。 最も多くワインを栽培する方法を教えてください。

これを最適化問題と呼んでいます。今回は、最適化問題の中でも凸最適化問題という手法を使って問題を解いていきます。

上の問題を解決するのは簡単ですが、実際に起こる問題はもっと複雑です。

問題を解決する方法をより深く理解するために、数学的基礎について少し紹介したいと思います。

少し数学について

最適化問題

最適化問題(optimization problem)とは、特定の集合上で定義された実数値関数または整数値関数についてその値が最小(もしくは最大)となる状態を解析する問題です。

最適化問題の公式は、

最適化問題

f(x) :目的関数 g_{i}(x), h_{j}(x) :制約条件

最適化問題では、大域的最適解 (globally optimal point) ではなく局所的最適解 (locally optimal point) だけが見つかる場合があります。それは、最高の解決策ではないかもしれません。

局所的最適解と大域的最適解

したがって、次のセクションでは、凸集合と凸最適化問題について議論し、その特殊な性質から、上記の問題を回避することができます。

凸集合

ユークリッド空間における物体が凸(convex)であるとは、その物体に含まれる任意の2点に対し、それら2点を結ぶ線分上の任意の点がまたその物体に含まれることを言います。

凸集合と非凸集合

集合 C が凸であるとき、任意の x, y ∈ C および任意の λ ∈ [0, 1] に対し、点 λx + (1 − λ)y もまた C に属することをいう。

凸最適化問題

凸最適化問題の公式は、

最適化問題

ここで、 - 関数 f, g_{i} は凸である。 - 関数 h_{j}アフィン変換、すなわち線形関数。

凸最適化問題では、局所的最適解(local optimal solution)は大域的最適解(global optimal solution)です。この性質は結果を確実にすることができる最善の選択肢です。

大域的最適解

問題の例

このセクションでは、凸最適化問題について簡単な例を見ましょう!

デモのために、Pythonと CVXOPT ライブラリ を使います。

線型計画法

線型計画法の公式は、

Linear Programming

この記事の冒頭の例は、線形計画法を利用して問題を解きましょう。

数学の実装のために、 x は葡萄の面積、 y は苺の面積です。

  • ワインの量は equation

  • 畑の面積は800平方メートルです。 equation

  • 葡萄栽培のコストは3千円、苺は千円、最大コストは1万8千円です。 equation

  • 葡萄の栽培時間は、100平方メートルあたり2日間、苺の栽培時間は100平方メートルあたり5日間です、最大で30日かかります。 equation

問題は次のように表されます。

equation

デモ

上記の式は次のように書き直すことができます。

equation

また、行列を使って上記の式を表すと以下のようになります

equation

from cvxopt import matrix, solvers

c = matrix([-50., -30.])
G = matrix([[1., 3., 2., -1., 0.], 
            [1., 1., 5., 0., -1.]])
h = matrix([8., 18., 30., 0., 0.])

solvers.options['show_progress'] = False
sol = solvers.lp(c, G, h)

print(sol['x'])

結果

[ 5.00e+00]
[ 3.00e+00]

最適な解決策は、500平方メートルの葡萄と300平方メートルの苺を植えます。なので、めぐみさんは340リットルのワインを作ることができます。

二次計画法

次は、二次計画法問題という線形計画法問題の拡大を見てみましょう。この問題の公式は、

Quadratic Programming

P = 0 なら線形計画問題になります。

次の問題を解きしましょう。

equation

デモ

上記の式は次のように書き直すことができます。

equation

また、行列を使って上記の式を表すと以下のようになります。

equation

from cvxopt import matrix, solvers

P = matrix([[2., 0.], [0., 2.]])
q = matrix([-50., -80.])
G = matrix([[1., 3., 2., -1., 0.], 
            [1., 1., 5., 0., -1.]])
h = matrix([8., 18., 30., 0., 0.])

solvers.options['show_progress'] = False
sol = solvers.qp(P, q, G, h)

print(sol['x'])

結果

[ 3.33e+00]
[ 4.67e+00]

よって、 equation のとき、 equation の値は最小です。

まとめ

  • 凸最適問題は非常に実用的な問題です。

  • このブログでは、凸最適化問題の2つの基本的な数学的モデル、線型計画法と二次計画法を紹介しました。

  • CVXOPTライブラリは、凸最適化問題を素早くきれいに解決するのに役立ちます。

あなたは多くの条件で問題を抱えたことがありますか?この記事がお役に立てば幸いです。

一緒に働いてみたいといった方もぜひお待ちしてます! 次回は都築さんが書く予定です。

お読みになっていただきありがとうございます!

参考文献

[1] ウィキペディア

[2] Convex Optimization – Boyd and Vandenberghe, Cambridge University Press, 2004

[3] Machine Learning co ban - Vu Huu Tiep, 2018

[4] CVXOPT

ReSwiftライブラリの紹介

はじめに、

こんにちは、エンジニアのDuyです。

現在、アプリを開発し続けるにつれて、MVCというアーキテクチャには弱点があらわれつつあります。システムが複雑になるにつれて、アプリの状態を管理しにくかったり、データの同期の問題もあります。その問題を解決したいという理由で、ReSwiftが作られました。今回アプリの開発に関して、ReSwiftを簡単に紹介します。

ReSwiftとは?

最近、JavaScriptのReduxというライブラリをよく耳にしますが、Swiftに関しても、Reduxのようなライブラリは存在するのか?答えは、存在します。

I have Redux. I have Swift. Ughhh! ReSwift!

f:id:curama-tech:20180410115849p:plain

そうです。ReSwiftとは、

ReSwift is a Redux-like implementation of the unidirectional data flow architecture in Swift.

詳細的には下の図を一緒に見ましょう。

f:id:curama-tech:20180409100121p:plain

図を見ながら、ReSwiftの主要なコンポネントを単に説明したいと思います。

The Store: アプリに対して、Storeはユニークです。アプリの現在のState(状態)を保存することがThe Storeの役割です。その状態を変更したいとき、必ず、Actionsをディスパッチしないといけない。 そして、Stateの値が変化したら、The StoreがすべてのSubscribersに通知を送信します。アプリの状態を全てStoreに保存するため、アプリの状態を管理しやすいです。

Actions: Actionはプレーンオブジェクトです。ActionがStateの変化を記述します。

Reducers: ReducersがActionとStateを受け取り、新しいStateを返します。

Subscribers: Storeに保存されるStateを変化したら、SubscribersがそのStateを画面に表示します。

イメージとしては、下記のようなフローになります。

  1. SubscribersがStoreにActionを発送します。
  2. StoreがそのActionを消費し、同時に、アプリの現在StateをReducer宛にフォワードします。
  3. ReducerがActionによって、新しくStateを作成して、StoreにそのStateを保存します。
  4. StoreのStateが変化したので、Subscribersが新しいStateをもらって、表示します。

現在、うちの会社ではたくさんのStateを管理します。例えば、カレンダーとか、メッセージとか、店舗の情報とかサービス等、それぞれ項目のStateを作って管理します。 実際の使った結果によって、アドバンテージがあります。

  • コンポネントの任務が分割できます。
  • アプリの状態はStoreしか管理しないので、データの同期の問題を解決でき、変化したら、すぐにSubscribers(Views)に反映される。
  • コンポネントを拡張しやすい。
  • Unidirectional data flowなので、ログして、テストしやすい。

シンプルプロジェクトを開発してみよう

実際に、どういうふうに実装しますかって言う質問がある方がいると思うので、Demoで説明したいと思います。

まずは、こちらのプログラムをダウンロードしましょう。

Note: Xcode 9 + swift3を使うDemoです。

ダウンロードが完了したら、Cocoapodで、ReSwiftをインストールします。 Cocoapodについて、こちらに参考できます。

ダウンロードしたリポジトリを開いて、Terminalでpod コマンドを実行します。

$ pod install

とりあえず、BlogContactDemo.xcworkspaceを開いて、ビルドして見ましょう。 結果は:

f:id:curama-tech:20180410122658p:plain:w240 f:id:curama-tech:20180410122716p:plain:w240

詳細画面で何か変更して、更新ボタンを押して、バックボタンを押してみたら、リスト内容は前の状態のままです。変更したところは反映されていなかったです。

変更した内容を反映したい時、NotificationCenterを観察して、内容が変わったら、表示するのも一つのやり方ですが、今回はReSwiftで処理します。

まずは、List画面のActionsを定義します。

// Actions/ContactListAction.swift

import ReSwift

struct RequestGetContactListAction: Action {}

struct ResponseGetContactListAction: Action {
    let model: [Contact]
}

extension ContactListState {
    static func getContactList() -> Store<AppState>.AsyncActionCreator {
        return { (state, store, callback) in
            store.dispatch(RequestGetContactListAction())
            callback { _,_ in ResponseGetContactListAction(model: ContactFixtures.currentData) }
        }
    }
}

ここは、2つActionを定義します。

  • RequestGetContactListActionには、Contactの一覧を習得したいというActionです。今回は簡単に説明したいので、APIを使わずに、Fixtureデータを使います。
  • ResponseGetContactListActionには、想定的にはAPIから結果をもらう時、結果を返しましたよというActionです。

次、List画面のReducerを定義します。

 // Reducers/ContactListReducer.swift

    import ReSwift

    struct ContactListReducer {}

    extension ContactListReducer {
        func handleAction(action: Action, state: ContactListState?) -> ContactListState {
            let prevState = state ?? ContactListState()
            var nextState = prevState
            
            switch action {
            case is RequestGetContactListAction:
                nextState.model = nil
            case let action as ResponseGetContactListAction:
                nextState.model = action.model
            default:
                break
            }
            
            return nextState
        }
    }

ContactListReducerにはそれぞれのContactList画面のActionをハンドリングして、新しいStateを返します。

詳細画面にはActionが2つあると思います。

    // Actions/ContactDetailAction.swift

    import ReSwift

    struct RequestUpdateContactDetailAction: Action {}

    struct ResponseUpdateContactDetailAction: Action {
        let model: Contact
    }

    extension ContactDetailState {
        static func updateContact(with id: Int, name: String, phone: String) -> Store<AppState>.AsyncActionCreator {
            return { (state, store, callback) in
                store.dispatch(RequestUpdateContactDetailAction())
                let contact = ContactFixtures.updateData(with: id, name: name, phone: phone)
                callback { _,_ in ResponseUpdateContactDetailAction(model: contact) }
            }
        }
    }

また、ContactDetailActionをハンドリングするため、ContactDetailReducerを作ります。

    // Reducers/ContactDetailReducer.swift

    import ReSwift

    struct ContactDetailReducer {}

    extension ContactDetailReducer {
        func handleAction(action: Action, state: ContactDetailState?) -> ContactDetailState {
            let prevState = state ?? ContactDetailState()
            var nextState = prevState
            
            switch action {
            case is RequestUpdateContactDetailAction:
                nextState.model = nil
            case let action as ResponseUpdateContactDetailAction:
                nextState.model = action.model
            default:
                break
            }
            
            return nextState
        }
    }

ContactListStateとContactDetaiStateにはAppStateの属性ですので、StoreにActionが発送される時AppStateを変化します。そのため、アプリのReducerを作りましょう。

    // Reducers/AppReducer.swift
    import ReSwift

    func appReducer(action: Action, state: AppState?) -> AppState {
        return AppState(
            contactListState: ContactListReducer().handleAction(action: action, state: state?.contactListState),
            contactDetailState: ContactDetailReducer().handleAction(action: action, state: state?.contactDetailState)
        )
    }

上にメンションしましたが、The Storeまだ見ていないですよね。では、Storeを作りましょう。 リマインダー:The Storeは唯一です。

ReSwift公式ページに対して、The Storeの定義はAppDelegateに書きます。

import UIKit
import ReSwift

var store = Store<AppState>(reducer: appReducer, state: nil) //追加したコード

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...

StateとかActionとかReducer等、全て定義しました。これから、使いかたの説明をします。

ControllersのContactListViewController.swiftにReswiftをimportして、下記のコードをファイルの最後に入れましょう。

    // ファイルの最後に書きます。
    extension ContactListViewController: StoreSubscriber {
        func newState(state: AppState) {
            if let model = state.contactListState.model {
                dataSources = model
            }
        }
    }

次はContactListViewControllerのclasの中にfetchDataとviewWillAppearとviewDidDisappearという関数を入れます。

    private func fetchData() {
        store.dispatch(ContactListState.getContactList())
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        store.subscribe(self)
        if store.state.contactListState.model == nil {
            fetchData()
        }
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        store.unsubscribe(self)
    }

dataSourcesの定義、少し変更します。

    fileprivate var dataSources: [Contact] = [] {
        didSet {
            tableView.reloadData()
        }
    }

ここで、アプリをビルドしてみます。一覧画面からセルを選択して、詳細画面に遷移します。名前と電話番号を変更して、更新ボタンをクリックして、バックボタンを押したら、 何も変わらないことがわかります。

Animated GIF - Find & Share on GIPHYgph.is

原因は、詳細画面にはまだStoreにsubscribeしていなかったので、StoreにActionをディスパッチされていない。

一覧画面みたいな書き方に応じて、ContactDetailViewController.swiftに下記のコードを入れます。

import:

import UIKit
import ReSwift //追加したコード

class ContactDetailViewController: UIViewController {
...

subscribe:

    // ファイルの最後に書きます
    extension ContactDetailViewController: StoreSubscriber {
        func newState(state: AppState) {
            if let _ = state.contactDetailState.model {
                let _ = navigationController?.popViewController(animated: true) // 更新したあとで、すぐに一覧画面に戻ります。
            }
        }
    }

と、

    ...

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }
    
    <b>
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        store.subscribe(self)
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        store.unsubscribe(self)
    }
    </b>
    private func setupViews() {
        view.backgroundColor = .white
    
    ...

ContactDetailViewControllerのupdateContact()関数には下記になります

    @objc private func updateContact() {
        // ボタンをくりっくして、見やすくしたいので、ボタンの色を変更します。
        submitBtn.backgroundColor = .darkGray
        view.endEditing(true)

        let name = detailView.nameTextField.text ?? ""
        let phoneNumber = detailView.phoneTextField.text ?? ""
        
        store.dispatch(ContactDetailState.updateContact(with: contact.id, name: name, phone: phoneNumber))

        // 前のボタンの色を戻します。
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in  
            self?.submitBtn.backgroundColor = .purple
        }
    }

最後のステップ、ContactListReducerにResponseUpdateContactDetailActionをハンドリングします。

    ...
    case is ResponseUpdateContactDetailAction:
        nextState.model = nil

    ...

じゃあ、実行してみよう!

詳細画面に変更した内容が反映されましたが、また、詳細画面に開いたら?

そうです。すぐに一覧画面に戻ってしまいます。

Animated GIF - Find & Share on GIPHYgph.is

原因は? ResponseUpdateContactDetailActionに発送したが、state.contactDetailStateのmodelが値を持っているので、すぐさま親の画面に戻ります。下記のコードの原因です。

    if let _ = state.contactDetailState.model {
        let _ = navigationController?.popViewController(animated: true)
    }

解決方法はContactDetailActions.swiftに新しいActionを追加します。 ResponseUpdateContactDetailActionの下にコードを入れます。

    ...

    struct RefreshContactDetailAction: Action {}
    ...

Actionがありました。ハンドリングしよう。ContactDetailReducerにはこういう感じになります。

import ReSwift

struct ContactDetailReducer {}

extension ContactDetailReducer {
    func handleAction(action: Action, state: ContactDetailState?) -> ContactDetailState {
        let prevState = state ?? ContactDetailState()
        var nextState = prevState
        
        switch action {
        case is RefreshContactDetailAction: //追加したコード
            nextState.model = nil //追加したコード
        case is RequestUpdateContactDetailAction:
            nextState.model = nil
        case let action as ResponseUpdateContactDetailAction:
            nextState.model = action.model
        default:
            break
        }
        
        return nextState
    }
}

後、ContactDetailViewControllerのviewDidDisappear関数にはこういう感じになります。

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    store.dispatch(RefreshContactDetailAction()) //追加したコード
    store.unsubscribe(self)
}

修正が完了しました。ビルドしてみたら、うまく動きました。

Animated GIF - Find & Share on GIPHYgph.is

完了したものはこちらです

終わりに

上記にReSwiftについて、初期的に紹介しました。ReSwiftに関して、ReSwift-RouterとかReSwiftRecorder等もあります。 さらにStoreのStateが変化したら、すぐにSubscriberに反映するのはReactive感があります。ReactiveProgramingとReSwiftを結合するのはReactiveReSwiftです。 読者がReSwiftの親戚に興味があれば、公式ページにサンプルコードを参考できます。

また、猫ちゃんの言いたいことがあります。

こちらは私の友達のシーちゃんです。

f:id:curama-tech:20180410123945j:plain

"技術について、興味がある方、ぜひ、しーちゃんのチームに参加しましょう。"

次回は、Tuyenさんによる凸最適化問題の応用の記事です。