はじめに
macOS向けアプリ開発をしてみるシリーズ。食わず嫌いはよくないと思います。
以前macOSアプリでListを作ろうとして、表計算ソフトのような表が現れたのをみて「そうじゃぁ無いんだ……」とがっかりしておりました。もしかすると、CocoaPodsを利用したりScrollViewを活用すればできたのかもしれないですが、当時はそこでiOSの方に興味が移ってしまったわけです。
月日が流れSwiftUIが登場した今、もしかしたらできるのではという淡い期待とともに、改めて挑戦してみようと思った次第です。
下準備
プロジェクトの準備
新しいプロジェクトを作っても良いのですが、せっかくなので流用します。
Vaporアプリケーション経由でMongoDBにアクセスし、本の情報を読み書きすることを目指すアプリケーションです。じわじわっと育てていきます。
方針
基本となるアプリケーションはすでに用意した段階からのスタートになります。
データモデルもすでに存在する状態です。
- ListViewの各行を表すViewを作る
- ListViewを作る
- Vaporアプリケーションに、本の情報一式をJSONで返してもらうRouteを作る
- ListViewに表示する
ひとまずここまで。
ListViewの各行をクリックすると詳細が表示されたり、絞り込み検索をしたり、情報の更新をしたりなどは次のステップです。
ListViewを表示することが今回の主目的です。
Viewを作っていく
各行を表すViewを作る
小さいところから作っていきます。まずはListViewに表示する各行のViewを作っていきます。
本の情報はひとまとめにして構造体として持たせるようにしています。Viewにその構造体をもたせられるようにしておきます。
struct BookRow: View {
var bookData:Book
var body: some View {
Text("stub")
}
}
併せてPreviewProvider
も調整しておきます。初期化ができていないとビルドエラーが出てしまいます。ダミーデータなので内容は適宜で大丈夫です。
struct BookRow_Previews: PreviewProvider {
static var previews: some View {
BookRow(bookData: Book.init(title: "TitleDummy", publisher: "PubDummy", price: 1234))
}
}
仕上げに本の情報を表示するためにViewを整えていきます。ここはプレビューを見ながら進めていきましょう。
VStack(alignment: .leading) {
Text(bookData.title)
.font(.body)
.fontWeight(.bold)
.opacity(1.0)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
HStack(alignment: .center) {
Text(bookData.publisher)
.font(.footnote)
.fontWeight(.regular)
.opacity(0.75)
.truncationMode(.middle)
.frame(maxWidth: .infinity, alignment: .leading)
ThousandsText(value: $bookData.price)
.font(.footnote)
.opacity(0.75)
.truncationMode(.middle)
.frame(maxWidth: 80, alignment: .trailing)
}.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: 48.0)
.padding(8.0)
モディファイアもりもりにしました。盛りすぎて見通しが悪くなったので、ほどほどの所でViewModifierにまとめてしまいたいです、
数値を3桁区切りで表示するViewを作っておく
ところでThousandsText
とは?
数値を3桁区切りで表示したいというのはよくある話です。ベタな方法は、都度Formatterを使って3桁区切りに変換することですが、せっかくなので、3桁区切りで表示するViewを作っておこうと思います。
使いまわしできて便利です。
さっき出てきたThousandsText
はこれです。
方針としては数値を渡すと3桁に区切って表示してくれるViewです。シンプル。
struct ThousandsText: View {
@Binding var value:Int
var body: some View {
Text(separate(value))
}
var separate = { (price:Int) -> String in
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.groupingSeparator = "," // 区切り文字をここで指定
formatter.groupingSize = 3 // 区切り文字を入れる桁数をここで指定
return formatter.string(from: NSNumber(integerLiteral: price)) ?? "\(price)"
}
}
表示させる値には、Binding属性を付けています。このViewを呼び出すViewで管理しているので。
ListView本体を作る
行を表すパーツができたので、それを並べるListViewを作っていきます。
なお、今回はMongoDBのドキュメントを表示させるので、MongoDBとのやり取りをさせるためのクラスを別途作っています。安直にAccess2Mongo
クラスと命名。
MongoDBから取得したドキュメント一式を持っているのが、そのクラスのプロパティbooks
です。books
の中身はただの配列なので、ForEachで1つずつループさせています。
基本はデータをForEachで順繰りに呼び出して、それをさっき作ったViewに渡しているだけです。
struct BookList: View {
@EnvironmentObject private var access2mongo:Access2Mongo
@Binding var selectedBook: Book?
var body: some View {
List(selection: $selectedBook) {
ForEach(access2mongo.books, id: \.self) { book in
BookRow(bookData: book)
}
}
}
}
VaporアプリケーションにRouteを追加する
Vaporアプリケーションを通してMongoDBからドキュメントを取得します。
Leafテンプレートにデータを渡してWebページを表示させることに興じていたため、ドキュメントそのものを取得するRouteがなくなっていました。なぜ消したのか。
復習も兼ねてVapor4対応でサックリと作ってみます。全件取得バージョン。
app.get("api","data","books") { req -> EventLoopFuture<[BookData]> in
let client = req.application.mongoClient
let collection = client.db("vaporapp").collection("books", withType: Book.self)
var books = [BookData]()
return collection.find().flatMap { cursor in
cursor.toArray()
}.flatMap { booksDocs -> EventLoopFuture<[BookData]> in
for bookDoc in booksDocs {
let book = BookData(id: bookDoc._id.hex, title: bookDoc.title, publisher: bookDoc.publisher, price: bookDoc.price)
books.append(book)
}
return req.eventLoop.future(books)
}
}
Routeパスが少し残念な事になっていますが、LeafテンプレートによるView生成と共存させるためにあえてこうしておきました。生のデータを取得するという意味でdata
ではなくraw
でも良いかもしれない。
命名にはいつも悩まされます。
ListViewを表示する
さてと。いよいよ最後です。ContentViewにListViewを表示させます。Viewの入れ子が続いていましたが、最後です。
そそっと並べて、モディファイアで整形です。
struct ContentView: View {
@EnvironmentObject private var access2mongo:Access2Mongo
@State var books:[Book] = []
@State private var selectedBook:Book?
var body: some View {
GeometryReader{ geometry in
VStack {
Text("Hello, World!")
Button(action: {
self.access2mongo.get()
}) {
Text("Get Document")
}
BookList(selectedBook: self.$selectedBook)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
}
}
}
動かそう
動かしてみましょう。
まずはウィンドウが開いた状態。
ここでGetDocumentボタンをクリックすると、データの取得とList表示がなされます。
まとめ
macOSでもiOSのようなListViewを作ることができました。
それにしても、ロジックを切り離してViewに集中できるのは幸せなことですな〜