身近な技術的課題から始めるOSSプロジェクト

  • このエントリーをはてなブックマークに追加

Yahoo! JAPAN Tech Advent Calendar 2017の14日目の記事です。一覧はこちら

こんにちは、iOSアプリ黒帯の林(@kazuhiro494949)です。

ヤフーでは、普段はiOSアプリの開発をしながらそこで得た技術的な知見を広く社会へ共有するという仕事をしています。

「技術的な知見を社会へ共有する」というのは具体的にはどういったことを指しているのでしょうか。その方法はさまざまあるかと思いますが、今回はOSSを通じた技術コミュニティーへの貢献という話を書きたいと思います。といっても、紹介する事例はSwiftRxSwiftfastlaneなどへコミットするという大きな話ではありません。もっと身近な、「日常の開発で発見したピンポイントな課題を解決するライブラリ」という観点で話を進めます。

モチベーション

1年以上前の話になります。try! SwiftでHiroki Katoさんが「Motivation based library abstraction」というタイトルのトークをされていました。ライブラリを作る時のモチベーションは一体どこから得るのかというトピックを扱っており、業務で直面した身近な課題から着想を得ているというというお話をされています。

私自身、似たような形で日々直面する技術的課題をヒントにライブラリ開発を行っています。そして、

  • 会社のプロダクトに使われているコードで世の中の技術的課題を解決するものは、OSSとして切り出す
  • 会社のプロダクトから広く世の中に通じる技術的課題を発見し、個人でOSSとして作ってフィードバックする

というサイクルをこれまで可能な範囲で取り入れるようにしてきました。

この記事では、上述のトークに習って実際に開発してきたOSSのライブラリを3つほどご紹介しつつ、自分がどんな技術的課題に直面してどのように解決しようとしたかお伝えできればと思います。

事例紹介

SwiftyXMLParser

https://github.com/yahoojapan/SwiftyXMLParser

iOSアプリでXMLパーサーといえば、XMLParserを使うことになるかと思います。XMLParserはSAX型のパーサーにあたるため、効率が良い反面、やや扱いにくいという性質を持っています。例えば、ちょっとしたXMLをパースするだけでもこれだけの量のコードを書く必要があります。

class ViewController: UIViewController, XMLParserDelegate {
    var isTarget = false

    override func viewDidLoad() {
        super.viewDidLoad()
        let doc = """
            <ResultSet>
                <Result>
                    <Hit index=\"1\"><Name>Item1</Name></Hit>
                    <Hit index=\"2\"><Name>Item2</Name></Hit>
                </Result>
            </ResultSet>
        """.data(using: .utf8)!
        let parser = XMLParser(data: doc)
        // XMLParserDelegateの実装が呼ばれるようにする
        parser.delegate = self
        // パースする
        parser.parse()
    }

    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        if elementName == "Name" {
            isTarget = true
        }
    }

    func parser(_ parser: XMLParser, foundCharacters string: String) {
        if isTarget {
            print(string) // Item1とItem2が出力される
        }
    }

    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        isTarget = false
    }
}

Yahoo!ショッピングのiOSアプリでは、APIレスポンスの一部にXMLが使われています。そして長年の積み重ねによって、パースするために書かれていたコードは大掛かりな読みにくいものになっていました。そこで、開発言語をSwiftに置き換えるタイミングだったこともあって、Swiftでパーサーを一から作ろうと思い立ちました。当時どのようにプロジェクトを進めたかは、以前執筆したTech Blogに詳細が載っています。

作ったライブラリは、レスポンスの変更にできるだけすぐ対応できるよう直観的なI/FにしたかったためDOM型のパーサーにしています。開発する際には、当時JSONのパーサーとしてデファクトとなっていたSwifyJSONのI/Fを参考にしました。以下がSwiftyJSONの利用例になります。

//Getting a string using a path to the element
let path: [JSONSubscriptType] = [1,"list",2,"name"]
let name = json[path].string

//Just the same
let name = json[1]["list"][2]["name"].string

//Alternatively
let name = json[1,"list",2,"name"].string 

// https://github.com/SwiftyJSON/SwiftyJSON より

SwiftyXMLParserではデータにアクセスするためにSubscriptsを利用していて、直観的にXMLのデータへアクセスできます。先程XMLParserで書いたパース処理をこのライブラリへ置き換えると、次のようになります。

class ViewController: UIViewController {    
    override func viewDidLoad() {
        let string = """
            <ResultSet>
                <Result>
                    <Hit index=\"1\"><Name>Item1</Name></Hit>
                    <Hit index=\"2\"><Name>Item2</Name></Hit>
                </Result>
            </ResultSet>
        """

        let xml = try! XML.parse(string)
        let text = xml["ResultSet", "Result", "Hit", 0, "Name"].text
        print(text) // => Item1
        for hit in xml["ResultSet", "Result", "Hit"] { 
            print(hit["Name"].text) 
        }
    }
}

もともとは特定のアプリ用に開発していたものだったのですが、実際に利用してみると意外と使い勝手がよく汎用的であることがわかりました。そこで、しばらく運用した後にヤフーのGitHubアカウント経由でOSSとして公開しました。

公開後に大きなアップデートはしていませんが、macOS対応やSwiftのバージョンアップ対応などのプルリクエストを社外から頂けるなどメンテナンス面で公開してよかったと考えています。

StringStylizer

https://github.com/kazuhiro4949/StringStylizer

このライブラリもYahoo!ショッピングのiOSアプリで発生していた課題を解決するために作りました。Yahoo!ショッピングのiOSアプリのUIは以下のように文字装飾がとてもたくさんあります。

(Yahoo!ショッピングのiOSアプリ内商品詳細画面)

細かな文字装飾を行う場合、NSAttributedStringを利用するのが一般的でしょう。

NSAttributedStringはもともとの出自(Core Text)もあって、かなり独特なインターフェースを持っています。また、Objective-C時代からの名残でしっかりとした型付けもされていません。以下のようにStringをキーとしてAny型の値を指定するとスタイルが決定されます。実装するとこのようになります。

let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) 

let head = NSMutableAttributedString(
    string: "Hoge",
    attributes: [
        .foregroundColor : UIColor.red,
        .font: UIFont.systemFont(ofSize: 14)
    ]
)
let tail = NSMutableAttributedString(
    string: "Fuga",
    attributes: [
        .foregroundColor : UIColor.blue,
        .font: UIFont(name: "Helvetica", size: 17)!
    ]
)
head.append(tail)
label.attributedText = head

柔軟に文字装飾ができる一方、NSAttributedStringが持つインターフェースの読みにくさ・書きにくさがアプリのコード全体に広がってしまっていました。そこで、Swiftの持っている言語機能を活かすことで、課題解決しようと考えました。今でこそ似たようなライブラリはたくさんありますが、当時はObjective-Cで書かれたライブラリはあったもののSwiftを活かした形で提供されているものが見当たらなかったため、一からライブラリを作りました。

このライブラリを使うと、上に書いたものがよりシンプルに書くことができます。

let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) 
label.attributedText = "Hoge".stylize().color(.redColor) .size(14).attr +
                       "Fuga".stylize().color(.blueColor).size(17).font(.Helvetica).attr

技術的な詳細は以下のQiita記事にまとめています。

より直観的な記述を実現するために、ジェネリクスを応用した「Phantom Type」や、Dictionaryでの指定からBuilderパターンへの変更などを行っています。

単機能であるがゆえに汎用性も高かっため、現在では他のプロダクトでも活用できています。

PagingKit

https://github.com/kazuhiro4949/PagingKit

このライブラリは、ニュース系のアプリでよくあるページングUIを実現するためのライブラリです。GYAO!のiOSアプリでは、いろいろな画面にまたがってページングUIが採用されています。

例えば、以下のように画面によって異なるスタイルのセグメントコントロールで、ページングを切り替えられるようになっています。

パターン1 パターン2
img img

このUIを実現するためにGYAO!ではサードパーティー製のライブラリを利用するという選択肢を取りました。

ページングを扱うために使えるライブラリ自体は数多く存在するのですが、多くはデザインに柔軟性がありません。アプリ内で画面ごとに最適化されたページングUIを実現するには、既存のライブラリでは対応しきれませんでした。

開発をすすめる中で、この再利用性に関する問題はUIライブラリで特に顕著に現れるものだと考え、どのようなクラス設計が妥当であるか検討してライブラリ化しました。

このライブラリを使うと、以下のようにさまざまなスタイルのページングUIが実現できます。

タグ テキストハイライト  下線 インジケーター
sample_3 sample_2 sample_1 indicator

kazuhiro4949/PagingKitより)

この辺りの開発に至った経緯やどのような課題にフォーカスして作ったかについて、「iOSDC Reject Conference」で話をする機会がありました。

iOSDC Reject Conference Day.1

発表やライブラリに対する反応を見る限り、自分自身が考える課題意識や解決策に対してある程度の評価は頂けていると感じています。そういったことは、しっかりと考えをまとめて外に対してアウトプットすることで初めて得られるものでしょう。

まとめ

今回3つの事例を紹介しつつ、「業務で発見した技術的課題をOSSとしてアウトプットする」ということについて書きました。初めから切り出すことを意識してクラス設計をすることで、機能が明確で疎結合なコードを書くことができます。さらに、普段から技術的な課題がないか意識してコードを書くことで、個人的にライブラリを作るきっかけが得られることもあります。また、退職した元同僚がプルリクエストをくれて、そこでやりとりするのもとても楽しいものです。

一方、開発にかかるコストはそれなりに高くつきます。Dynamic Libraryをたくさん作ってしまうと起動時間に問題が発生する可能性もあります。

従って、作る際にはオーバーエンジニアリングにならないよう、本当に解決するべき課題なのかしっかり考えておく必要があります。

そして当たり前のことかもしれませんが、作る側はライセンスを用意する・利用する側としてはライセンスをチェックすることがとても大切です。他のライブラリから一部コードを持ってきているタイプのライブラリもよく見てみると意外と多くあります。正しく利用されているか、自分たちが使っても問題ないものなのか、ファイル単位でライセンスをチェックしたほうがよいでしょう。例えば有名なライブラリだと、MITライセンスで提供されているKingfisherの一部にZlibライセンスのコードがコピーされていたことがありました。

iOSアプリ開発も歴史が長く大規模化・複雑化が進み、サードパーティー製のライブラリに頼らず作るのが難しくなっています。私も多くのOSSに助けられていますし、これからも世の中の技術的課題を解決するユニークなライブラリがどんどん出てくることを楽しみにしています。そういったライブラリを開発する時に、この記事がほんの少しでもお役に立てば幸いです。

Yahoo! JAPAN Tech Advent Calendar 2017の14日目は以上となります。

参考資料

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加