#author("2022-07-22T08:15:58+00:00","default:src128","src128") #author("2022-07-22T08:16:56+00:00","default:src128","src128") &tag(SwiftUI/日本語チュートリアル2); *目次 [#o4046361] #contents *関連ページ [#v37374c7] *参考情報 [#de50f83f] -[[【第2回】日本語版SwiftUIチュートリアル【リストとナビゲーション】 | HIRO LAB BLOG:https://hirlab.net/nblog/category/programming/art_1440/]] *第2回:リストとナビゲーション ~動的なビュー生成~ [#he2a741d] ** Step 1. モデルを作る [#cde9f629] -Modelsディレクトリを作成。 -そのなかにLandmark.swiftを作る。 #pre{{ import SwiftUI struct Landmark: Hashable, Codable{ var id: Int var name: String fileprivate var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D( latitude: coordinates.latitude, longitude: coordinates.longitude) } enum Category: String, CaseIterable, Hashable, Codable{{ case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" case mountains= "Mountains" } } extension Landmark { var image: Image { ImageStore.shared.image(name: imageName) } } struct Coordinates: Hashable, Codable { var latitude: Double var longitude: Double } }} ** Step 2. データ処理部 [#ub7fa690] -ModelsディレクトリにData.swiftを作成。内容は以下。 #pre{{ import UIKit import SwiftUI import CoreLocation let landmarkData: [Landmark] = load("landmarkData.json") func load<T: Decodable>(_ filename: String) -> T { let data: Data guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else { fatalError("Couldn't find \(filename) in main bundle.") } do { data = try Data(contentsOf: file) } catch { fatalError("Couldn't load \(filename) from main bundle:\n\(error)") } do { let decoder = JSONDecoder() return try decoder.decode(T.self, from: data) } catch { fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") } } final class ImageStore { typealias _ImageDictionary = [String: CGImage] fileprivate var images: _ImageDictionary = [:] fileprivate static var scale = 2 static var shared = ImageStore() func image(name: String) -> Image { let index = _guaranteeImage(name: name) return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(name)) } static func loadImage(name: String) -> CGImage { guard let url = Bundle.main.url(forResource: name, withExtension: "jpg"), let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { fatalError("Couldn't load image \(name).jpg from main bundle.") } return image } fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index { if let index = images.index(forKey: name) { return index } images[name] = ImageStore.loadImage(name: name) return images.index(forKey: name)! } } }} -要はjsonからLandmarkオブジェクトを作成するたに使われている。 **Step 3. リストビューを作る前に構成要素を作る [#q3807f8b] -ContentView.swiftをLandmarkDetail.swiftに変更。リファクタリング機能を使う。 -LandmarkRowを作成。 #pre{{ import SwiftUI struct LandmarkRow: View { var landmark: Landmark var body: some View { Text(landmark.name) } } struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { LandmarkRow(landmark: landmarkData[0]) } } }} -デザインを整える。これでリストに表示可能な見た目となる。 #pre{{ struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack{ landmark.image .resizable() .frame(width: 50, height: 50) Text(landmark.name) Spacer() } } } }} **Step 4. 複数のデータを使ったプレビュー表示 [#k62e76cd] -プレビューの改善。 #pre{{ struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { Group { LandmarkRow(landmark: landmarkData[0]) LandmarkRow(landmark: landmarkData[1]) LandmarkRow(landmark: landmarkData[2]) } .previewLayout(.fixed(width: 300, height: 70)) } } }} **Step 5. 観光地データをリストとして積み上げる [#l70eca08] -LandmarkList.swiftを作成。 -以下で2個のデータが表示される。 #pre{{ import SwiftUI struct LandmarkList: View { var body: some View { List { LandmarkRow(landmark: landmarkData[0]) LandmarkRow(landmark: landmarkData[1]) } } } struct LandmarkList_Previews: PreviewProvider { static var previews: some View { LandmarkList() } } }} -動的データを表示する。 #pre{{ struct LandmarkList: View { var body: some View { List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } } } }} -これを可能とするためLandmarkにIdentifiableを付与。 struct Landmark: Hashable, Codable, Identifiable{ -これでリストが表示される。 -初期化のところで使われているのはクロージャ構文。 **Step 6. リスト<=>詳細ビューのナビゲーション [#h5d7ddab] -LandmarkList.swiftとLandmarkDetail.swiftをつなげる -LandmarkListナビゲーションビューとしタイトルをつける。 #pre{{ struct LandmarkList: View { var body: some View { NavigationView{ List(landmarkData) { landmark in LandmarkRow(landmark: landmark) }.navigationBarTitle(Text("Landmarks")) } } } }} -詳細ビューが表示できるようにする。 #pre{{ struct LandmarkList: View { var body: some View { NavigationView{ List(landmarkData) { landmark in NavigationLink(destination: LandmarkDetail()) { LandmarkRow(landmark: landmark) } }.navigationBarTitle(Text("Landmarks")) } } } }} -ただしまだ詳細ビューはTurtle Rockのまま。 **Step 7. 各要素を動的なビューにしていく [#a3d92232] - CircleImage.swiftを動的にする。 #pre{{ struct CircleImage: View { var image: Image var body: some View { image .clipShape(Circle()) .overlay( Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 10) } } struct CircleImage_Previews: PreviewProvider { static var previews: some View { CircleImage(image: Image("turtlerock")) } } }} -MapView.swiftを修正。 #pre{{ struct MapView: UIViewRepresentable { var coordinate: CLLocationCoordinate2D func makeUIView(context: Context) -> MKMapView { MKMapView(frame: .zero) } func updateUIView(_ uiView: MKMapView, context: Context) { let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0) let region = MKCoordinateRegion(center: coordinate, span: span) uiView.setRegion(region, animated: true) } } struct MapView_Previews: PreviewProvider { static var previews: some View { MapView(coordinate: landmarkData[0].locationCoordinate) } } }} -LandmarkDetail.swiftを修正。 #pre{{ struct LandmarkDetail: View { var landmark: Landmark var body: some View { VStack { MapView(coordinate: landmark.locationCoordinate) .edgesIgnoringSafeArea(.top) .frame(height: 300) CircleImage(image: landmark.image) .offset(y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { Text(landmark.name) .font(.title) HStack { Text(landmark.park) .font(.subheadline) Spacer() Text(landmark.state) .font(.subheadline) } } .padding() Spacer() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { LandmarkDetail(landmark: landmarkData[0]) } } }} -LandmarksAppを修正(SceneDelegate.swiftは存在しなかった)。 #pre{{ import SwiftUI @main struct LandmarksApp: App { var body: some Scene { WindowGroup { LandmarkList() } } } }} -LandmarkList.swiftを修正。 #pre{{ struct LandmarkList: View { var body: some View { NavigationView{ List(landmarkData) { landmark in NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } }.navigationBarTitle(Text("Landmarks")) } } } }} **Step 8. プレビュー端末を変える。 [#s3c13948] -以下のように変更できる。 #pre{{ struct LandmarkList_Previews: PreviewProvider { static var previews: some View { LandmarkList() .previewDevice(PreviewDevice(rawValue: "iPhone X")) } } }}