こちらの記事は以前にNewsPicks Tech Guideに投稿された記事をインポートしたものです。元の記事はid:kohei1218によって書かれました。
こんにちは、WWDC4日目に参加しておりますiOSエンジニアの齋藤です。
本日木曜は、夜19時からbashという野外音楽フェスが行われるので早めにこの記事を書いております。
bashの模様は明日の記事であげますね。
さて、今回はSwiftUIチュートリアルの2に触れていきます。
Building Lists and Navigation
※developerアカウントが必要ない、一般に公開されている内容になります。
はじめに
作成する内容はListが表示され、rowをタップすると詳細に遷移するアプリです。ListとはTableViewライクなViewです。
これ、実装してみて本当に驚きました。わざわざItemの数を管理しなくてよかったり、Selectの画面遷移が1行で書けたりとTableViewのかゆいところに見事に手が届いています。
今後のUI作成にかかる時間とコードは驚くほどに短縮されるのではないでしょうか。
Keynoteで発表された際の長いTableViewのコードが数行になるプレゼン、流石に多少盛っているかと思ったのですがまさかのそのままでした。
実装していて、インタラクティブかつスピーディーでにやけてしまいます。
今回はAppleがListに表示するデータやチュートリアル1で作成したView(今回の詳細のViewがこれになる)を用意してくれたプロジェクトファイルがあるのでこれを使用しましょう。
プロジェクトをダウンロードするとCompleteとStartingPointというフォルダがあるのでStartingPointを開き、->Landmarks->BuildingListsAndNavigation.xcodeprojを開きます。
早速チュートリアルの順に中身を見ていきましょう。
チュートリアル1で作成したViewがLandmarkDetailという名前であり、これが詳細のViewに当たりそうです。
次にModelフォルダを見ていきましょう。
LandmarkとDataというファイルがあります。
LandmarkはCodableを継承したModelのクラスになっており、DataはjsonファイルからLandmarkの配列を生成して返すものになっています。
Rowの実装
まずはListのRowを作成しましょう。TableViewでいうCellに当たります。
cmd+Nで新規ファイル->SwifUI Viewを選択、LandmarkRowを作成します。
次にLandmarkのpropertyをおきます。
struct LandmarkRow : View { var landmark: Landmark var body: some View { Text("Hello World!") } }
そうするとLandmarkRowの初期化にLandmarkのインスタンスが必要になるのでPreviewの方も修正します。
struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { // landmarkDataはjsonからdecodeされたLandmarkの配列 LandmarkRow(landmark: landmarkData[0]) } }
そして、TextをHStackViewの入れ子にし、上にlandmarkのimageをセット、Textにはlandmarkのnameをセットしてあげましょう。
struct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image(forSize: 50) Text(landmark.name) } } }
これだけでCanvasのプレビューにはLandmarkRow_PreviewsでセットしたlandmarkData[0]のimageと名前が表示されています。うーむ、すごい。
ちなみにLandmarkのimageはDataのImageStoreというクラスがjsonのimageNameからImageを生成して返しています。
final class ImageStore { fileprivate typealias _ImageDictionary = [String: [Int: CGImage]] fileprivate var images: _ImageDictionary = [:] fileprivate static var originalSize = 250 fileprivate static var scale = 2 static var shared = ImageStore() func image(name: String, size: Int) -> Image { let index = _guaranteeInitialImage(name: name) let sizedImage = images.values[index][size] ?? _sizeImage(images.values[index][ImageStore.originalSize]!, to: size * ImageStore.scale) images.values[index][size] = sizedImage return Image(sizedImage, scale: Length(ImageStore.scale), label: Text(verbatim: name)) }
あとはLandmarkRow_Previewsで色々なパターンを見ていきましょう。
previewsのLandmarkRow(landmark: landmarkData[0])の箇所をcmdを押しながらクリックしてEmbed in Listを選択します。
あとは渡されるitemでlandmarkDataからLandmarkを取ればListのUIを確認できます。これだけでもうTableViewのUIできてますね。
struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { List(0 ..< 5) { item in LandmarkRow(landmark: landmarkData[item]) } } }
Listの実装
Rowができたので次はListを実装していきます。
いつも通りSwift UI新規ファイル作成、今回はLandmarkListを作成します。
次にTextを消してListをおきます。Listはidentifiableなdataを扱います。なのでidentified(by:)を使用してidentifiableにするか、dataをIdentifiable protocolに準拠させる必要があります。
struct LandmarkList : View { var body: some View { List(landmarkData.identified(by: \.id)) { landmark in } } }
or
// Identifiableを準拠 struct Landmark: Hashable, Codable, Identifiable { ... } struct LandmarkList : View { var body: some View { List(landmarkData) { landmark in } } }
あとは先程作成したLandmarkRowをListの中に置いてあげればこれだけでListが完成しました。TableViewの労力とはえらい違いですね。
struct LandmarkList: View { var body: some View { List(landmarkData.identified(by: \.id)) { landmark in LandmarkRow(landmark: landmark) } } }
Navigationの実装
Listの親にNavigationViewをセットするだけで実装できます。
struct LandmarkList: View { var body: some View { NavigationView { List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } } } }
Navigationのtitleのセットもこれだけ
struct LandmarkList: View { var body: some View { NavigationView { List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } .navigationBarTitle(Text("Landmarks")) } } }
また自分が面白いと感じたのはタップ時の遷移でした。
RowをNavigationButtonの入れ子にし、destinationに遷移先をセットするだけでタップ時の遷移を実現できてしまいます。
struct LandmarkList: View { var body: some View { NavigationView { List(landmarkData) { landmark in NavigationButton(destination: LandmarkDetail()) { LandmarkRow(landmark: landmark) } } .navigationBarTitle(Text("Landmarks")) } } }
子Viewへのデータ受け渡し
最後に子のViewにデータを渡してあげてそれを表示してあげて、RootのViewを変更すれば完成です。
(コメントを書いてある箇所を追記しています。)
CircleImage
struct CircleImage: View { // imageのpropertyをセット var image: Image var body: some View { image .clipShape(Circle()) .overlay(Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 10) } } struct CircleImage_Preview: PreviewProvider { static var previews: some View { // previewも修正 CircleImage(image: Image("turtlerock")) } }
MapView
struct MapView: UIViewRepresentable { // CLLocationCoordinate2Dのpropertyをセット var coordinate: CLLocationCoordinate2D func makeUIView(context: Context) -> MKMapView { MKMapView(frame: .zero) } func updateUIView(_ view: MKMapView, context: Context) { let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02) let region = MKCoordinateRegion(center: coordinate, span: span) view.setRegion(region, animated: true) } } struct MapView_Preview: PreviewProvider { static var previews: some View { // previewも修正 MapView(coordinate: landmarkData[0].locationCoordinate) } }
LandmarkDetail
struct LandmarkDetail: View { var landmark: Landmark var body: some View { VStack { // CLLocationCoordinate2Dを渡す MapView(coordinate: landmark.locationCoordinate) .frame(height: 300) // Imageを渡す CircleImage(image: landmark.image(forSize: 250)) .offset(y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { Text(landmark.name) .font(.title) HStack(alignment: .top) { Text(landmark.park) .font(.subheadline) Spacer() Text(landmark.state) .font(.subheadline) } } .padding() Spacer() } // titleもセット .navigationBarTitle(Text(landmark.name), displayMode: .inline) } } struct LandmarkDetail_Preview: PreviewProvider { static var previews: some View { LandmarkDetail(landmark: landmarkData[0]) } }
LandmarkList
struct LandmarkList: View { var body: some View { NavigationView { List(landmarkData) { landmark in // landmarkを渡してあげる NavigationButton(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } .navigationBarTitle(Text("Landmarks")) } } } struct LandmarkList_Previews: PreviewProvider { static var previews: some View { LandmarkList() } }
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let window = UIWindow(frame: UIScreen.main.bounds) // rootのViewを変更 window.rootViewController = UIHostingController(rootView: LandmarkList()) self.window = window window.makeKeyAndVisible() } // ... }
お疲れ様でした。たったこれだけでNavigationのList、詳細のViewが完成してしまいました。いよいよTableViewとの決別を感じさせますね。
さて、明日はいよいよ最終日、WWDCのまとめとおすすめな過ごし方を紹介します!