Yahoo! JAPAN Tech Advent Calendar 2017の10日目の記事です。一覧はこちら
こんにちは、iOSアプリ黒帯の林(@kazuhiro494949)です。
ヤフーでは、普段はiOSアプリの開発をしながらそこで得た技術的な知見を広く社会へ共有するという仕事をしています。
「技術的な知見を社会へ共有する」というのは具体的にはどういったことを指しているのでしょうか。その方法はさまざまあるかと思いますが、今回はOSSを通じた技術コミュニティーへの貢献という話を書きたいと思います。といっても、紹介する事例はSwiftやRxSwift・fastlaneなどへコミットするという大きな話ではありません。もっと身近な、「日常の開発で発見したピンポイントな課題を解決するライブラリ」という観点で話を進めます。
モチベーション
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: CGRectMake(0,0,100,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: CGRectMake(0,0,100,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 |
---|---|
このUIを実現するためにGYAO!ではサードパーティー製のライブラリを利用するという選択肢を取りました。
ページングを扱うために使えるライブラリ自体は数多く存在するのですが、多くはデザインに柔軟性がありません。アプリ内で画面ごとに最適化されたページングUIを実現するには、既存のライブラリでは対応しきれませんでした。
開発をすすめる中で、この再利用性に関する問題はUIライブラリで特に顕著に現れるものだと考え、どのようなクラス設計が妥当であるか検討してライブラリ化しました。
このライブラリを使うと、以下のようにさまざまなスタイルのページングUIが実現できます。
タグ | テキストハイライト | 下線 | インジケーター |
---|---|---|---|