編集画面をつくる
チュートリアルより
まずプロフィールのデータ構造は
struct Profile { var username: String var prefersNotifications: Bool var seasonalPhoto: Season var goalDate: Date // デフォルト設定 static let `default` = Self(username: "g_kumar", prefersNotifications: true, seasonalPhoto: .winter) init(username: String, prefersNotifications: Bool = true, seasonalPhoto: Season = .winter) { self.username = username self.prefersNotifications = prefersNotifications self.seasonalPhoto = seasonalPhoto self.goalDate = Date() } // enumにCaseIterableつけとく enum Season: String, CaseIterable { case spring = "🌷" case summer = "🌞" case autumn = "🍂" case winter = "☃️" } }
編集画面にある日付Pickerの日付指定の範囲の作成が面白い
var dateRange:ClosedRange<Date> { let min = Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate)! let max = Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate)! return min...max }
編集画面は
import SwiftUI struct ProfileEditor: View { // 双方向バインディング @Binding var profile:Profile var dateRange:ClosedRange<Date> { let min = Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate)! let max = Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate)! return min...max } var body: some View { List { HStack { Text("UserName").bold() Divider() // ユーザープロファイル TextField("Username", text: $profile.username) } Toggle(isOn:$profile.prefersNotifications) {Text("Enable Notifications")} VStack(alignment: .leading, spacing: 20) { Text("Seasonal Photo").bold() // ピッカー、季節を選ぶ。セグメント Picker("Seasonal Photo", selection: $profile.seasonalPhoto) { ForEach(Profile.Season.allCases, id: \.self) { season in Text(season.rawValue ).tag(season)} }.pickerStyle(SegmentedPickerStyle()) } .padding(.top) // 日付ピッカー VStack(alignment: .leading, spacing: 20) { Text("Goal Date").bold() DatePicker( "Goal Date", selection: $profile.goalDate, in: dateRange, displayedComponents: .date ) } .padding(.top) } } } struct ProfileEditor_Previews: PreviewProvider { static var previews: some View { ProfileEditor(profile: .constant(.default)) } }
プロフィール画面の全体は
import SwiftUI struct ProfileHost: View { /// editModeはEnvironmentValuesでもともとある環境変数で、エディットモードであるかないか @Environment(\.editMode) var mode /// 環境変数、.environmentObject(UserData())で渡される。 @EnvironmentObject var userData: UserData @State var draftProfile = Profile.default var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { /// キャンセルボタンは編集モードのときのみ出る if self.mode?.wrappedValue == .active { Button("Cancel") { /// キャンセルするとドラフトはユーザープロフィールに戻る self.draftProfile = self.userData.profile self.mode?.animation().wrappedValue = .inactive } } Spacer() EditButton() } /// 編集画面かそうでないかの出し分けをする if self.mode?.wrappedValue == .inactive { ProfileSummary(profile: draftProfile)// 表示はドラフトを使う } else { ProfileEditor(profile: $draftProfile)// 表示はドラフトを使う .onAppear { /// 編集画面が表示されるとき、ユーザープロフィールをドラフトにする self.draftProfile = self.userData.profile } .onDisappear { /// 編集画面が消える時にドラフトをユーザーのプロフィールにする self.userData.profile = self.draftProfile } } } .padding() } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost().environmentObject(UserData()) } }
画面遷移をつける
チュートリアルより ホーム画面から遷移するコードを書く。
struct CategoryHome: View { // リストをcategory名をキーに辞書化している。 var categories: [String :[Landmark]] { Dictionary ( grouping: landmarkData, by: { $0.category.rawValue } ) } // お気に入りリスト var featuered : [Landmark] { landmarkData.filter { $0.isFeatured } } @State var showingProfile = false // プロフィールボタン var profileButton: some View { Button(action: { self.showingProfile.toggle() }) { Image(systemName: "person.crop.circle") .imageScale(.large) .accessibility(label: Text("User Profile")) .padding() } } var body: some View { // ここで囲まれている部分が画面遷移する。 NavigationView { List { // お気に入りは大きめにする。 FeaturedLandmarks(landmarks: featuered) .scaledToFill() .frame(height:200) .clipped() .listRowInsets(EdgeInsets()) // 各カテゴリーごとに縦にリスト ForEach(categories.keys.sorted(), id: \.self) {key in CategoryRow(categoryName: key, items: self.categories[key]!) } .listRowInsets(EdgeInsets()) // 画面の左右の余白の削除 // 画面遷移 NavigationLink(destination: LandmarkList()) { Text("See All") } } .navigationBarTitle("Featured") .navigationBarItems(trailing: profileButton) // プロフィールボタンをナビバーの右側に配置 .sheet(isPresented: $showingProfile) { Text("User Profile") // プロフィール画面をシートで出す } } } } // お気にいりの画像はリサイズ struct FeaturedLandmarks: View { var landmarks: [Landmark] var body: some View { landmarks[0].image.resizable() } } struct CategoryHome_Previews: PreviewProvider { static var previews: some View { CategoryHome() } }
struct CategoryRow: View { var categoryName: String var items: [Landmark] var body: some View { VStack { // カテゴリ名 Text(self.categoryName) .font(.headline) .padding(.leading, 15) .padding(.top, 5) // 横スクロール ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: 0.0 ) { ForEach(self.items) { landmark in // 詳細画面へのリンク NavigationLink(destination: LandmarkDetail(landmark: landmark)) { CategoryItem(landmark:landmark) } } } } .frame(height:185) } } } struct CategoryItem: View { var landmark: Landmark var body: some View { VStack(alignment: .leading) { landmark.image .renderingMode(.original) .resizable() .frame(width: 155, height: 155) .cornerRadius(5) Text(landmark.name) .foregroundColor(.primary) .font(.caption) } .padding(.leading, 15) } } struct CategoryRow_Previews: PreviewProvider { static var previews: some View { CategoryRow(categoryName: landmarkData[0].category.rawValue, items: Array(landmarkData.prefix(3)) ) } }
アニメーションをつける
チュートリアルより アニメーションは.animationで簡単につけられる。 .animation(nil)でアニメーションを切ることができる。 withAnimationを使うと、ブロック内にアニメーションがつけられる。 遷移時の遷移方法ははtransisionで指定できる。 transisionはAnyTransitionとして自作できる。 例えば表示されるときと消えるときで次のように定義できる。
extension AnyTransition { static var moveAndFade: AnyTransition { let insertion = AnyTransition.move(edge: .trailing) let removal = AnyTransition.scale .combined(with: .opacity) return asymmetric(insertion: insertion, removal: removal) } }
チュートリアルではボタンを押すとグラフがあらわれ、グラフが波打つ。
まず、グラフにつかうデータモデル
struct Hike: Codable, Hashable, Identifiable { var name: String var id: Int var distance: Double var difficulty: Int var observations: [Observation] static var formatter = LengthFormatter() var distanceText: String { return Hike.formatter .string(fromValue: distance, unit: .kilometer) } struct Observation: Codable, Hashable { var distanceFromStart: Double var elevation: Range<Double> var pace: Range<Double> var heartRate: Range<Double> } }
そしてグラフの描画部分
func rangeOfRanges<C: Collection>(_ ranges: C) -> Range<Double> where C.Element == Range<Double> { guard !ranges.isEmpty else { return 0..<0 } let low = ranges.lazy.map { $0.lowerBound }.min()! let high = ranges.lazy.map { $0.upperBound }.max()! return low..<high } func magnitude(of range: Range<Double>) -> Double { return range.upperBound - range.lowerBound } // 波打つアニメーションrippleを定義、これはスプリングを減衰率を指定し、ゆっくり、また値に応じたdelayをつけるという工夫をしている。 extension Animation { static func ripple(index:Int) -> Animation { Animation.spring( dampingFraction: 0.5) .speed(2) .delay(0.03 * Double(index)) } } struct HikeGraph: View { var hike: Hike var path: KeyPath<Hike.Observation, Range<Double>> var color: Color { switch path { case \.elevation: return .gray case \.heartRate: return Color(hue: 0, saturation: 0.5, brightness: 0.7) case \.pace: return Color(hue: 0.7, saturation: 0.4, brightness: 0.7) default: return .black } } var body: some View { let data = hike.observations let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: self.path] }) let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()! let heightRatio = (1 - CGFloat(maxMagnitude / magnitude(of: overallRange))) / 2 return GeometryReader { proxy in HStack(alignment: .bottom, spacing: proxy.size.width / 120) { ForEach(data.indices) { index in GraphCapsule( index: index, height: proxy.size.height, range: data[index][keyPath: self.path], overallRange: overallRange) .colorMultiply(self.color) .transition(.slide) // スライド .animation(.ripple(index: index)) // ここで波打つ } .offset(x: 0, y: proxy.size.height * heightRatio) } } } }
GraphCapsuleというのはひとまず、無視するが一応
struct GraphCapsule: View { var index: Int var height: CGFloat var range: Range<Double> var overallRange: Range<Double> var heightRatio: CGFloat { max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15) } var offsetRatio: CGFloat { CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var body: some View { Capsule() .fill(Color.white) .frame(height: height * heightRatio) .offset(x: 0, y: height * -offsetRatio) } } struct GraphCapsule_Previews: PreviewProvider { static var previews: some View { GraphCapsule(index: 0, height: 150, range: 10..<50, overallRange: 0..<100) } }
グラフを表示する詳細画面 dataToShowを動的に変える。
struct HikeDetail: View { let hike: Hike @State var dataToShow = \Hike.Observation.elevation var buttons = [ ("Elevation", \Hike.Observation.elevation), ("Heart Rate", \Hike.Observation.heartRate), ("Pace", \Hike.Observation.pace), ] var body: some View { return VStack { HikeGraph(hike: hike, path: dataToShow) .frame(height: 200) HStack(spacing: 25) { ForEach(buttons, id: \.0) { value in Button(action: { self.dataToShow = value.1 }) { Text(value.0) .font(.system(size: 15)) .foregroundColor(value.1 == self.dataToShow ? Color.gray : Color.accentColor) .animation(nil) } } } } } }
項目からタップしてグラフを出すようにする。 transisionを定義して使っている
extension AnyTransition { static var moveAndFade: AnyTransition { let insertion = AnyTransition.move(edge: .trailing) let removal = AnyTransition.scale .combined(with: .opacity) return asymmetric(insertion: insertion, removal: removal) } } struct HikeView: View { var hike: Hike @State private var showDetail = true var body: some View { VStack { HStack { HikeGraph(hike: hike, path: \.elevation) .frame(width: 50, height: 30) .animation(nil) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5:1.0) .padding() } } if showDetail { HikeDetail(hike: hike) .transition(.moveAndFade) } } } }
アイコンを作る
チュートリアルより。 背景に6角形を描き、その上に山のシンボルを角度をかえてぐるっと8個放射状に配置する。ZStackを使って重ねている。 放射状に配置するのはForEachで、サイズをハードコーディングを避けるにはGeometryReaderを用いている。 ポイントは様々なビューを作って組み合わせているところ。
まず、背景の6角形
struct BadgeBackground: View { var body: some View { GeometryReader { geometry in Path { path in var width:CGFloat = min(geometry.size.width, geometry.size.height) let height = width let xScale: CGFloat = 0.832 let xOffset = (width * (1.0 - xScale)) / 2.0 width *= xScale path.move(to: CGPoint(x: xOffset + width * 0.95, y: height * (0.20 + HexagonParameters.adjustment))) HexagonParameters.points.forEach { path.addLine(to: .init(x:xOffset + width * $0.useWidth.0 * $0.xFactors.0, y:height * $0.useHeight.0 * $0.yFactors.0)) path.addQuadCurve(to: .init( x: xOffset + width * $0.useWidth.1 * $0.xFactors.1, y: height * $0.useHeight.1 * $0.yFactors.1), control: .init( x:xOffset + width * $0.useWidth.2 * $0.xFactors.2, y:height * $0.useHeight.2 * $0.yFactors.2)) } } .fill(LinearGradient(gradient: .init(colors: [Self.gradientStart, Self.gradientEnd]), startPoint: .init(x: 0.5, y: 0), endPoint: .init(x: 0.5, y: 0.6))) .aspectRatio(1, contentMode: .fit) } } static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255) static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255) }
次に山のシンボル
struct BadgeSymbol: View { static let symbolColor = Color(red: 79.0 / 255, green: 79.0 / 255, blue: 191.0 / 255) var body: some View { GeometryReader { geometry in Path { path in let width = min(geometry.size.width, geometry.size.height) let height = width * 0.75 let spacing = width * 0.030 let middle = width / 2 let topWidth = 0.226 * width let topHeight = 0.488 * height path.addLines([ CGPoint(x: middle, y: spacing), CGPoint(x: middle - topWidth, y: topHeight - spacing), CGPoint(x: middle, y: topHeight / 2 + spacing), CGPoint(x: middle + topWidth, y: topHeight - spacing), CGPoint(x: middle, y: spacing) ]) path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3)) path.addLines([ CGPoint(x: middle - topWidth, y: topHeight + spacing), CGPoint(x: spacing, y: height - spacing), CGPoint(x: width - spacing, y: height - spacing), CGPoint(x: middle + topWidth, y: topHeight + spacing), CGPoint(x: middle, y: topHeight / 2 + spacing * 3) ]) } .fill(Self.symbolColor) } } }
山を回転させるのに一つビューを作っている。
struct RotatedBadgeSymbol: View { let angle:Angle var body: some View { BadgeSymbol() .padding(-60) .rotationEffect(angle, anchor: .bottom) } }
最後にzstackで組み合わせる。 全体サイズをGeometryReaderで調整し、バッジのサイズはscaleEffectで調整している点に注意。 ForEachを用いてる点も。
struct Badge: View { static let rotationCount = 8 var badgesymbols : some View { ForEach(0..<Badge.rotationCount) { i in RotatedBadgeSymbol(angle: .init(degrees: Double(i)/Double(Badge.rotationCount) * 360.0)) } .opacity(0.5) } var body: some View { ZStack { BadgeBackground() GeometryReader { geometry in self.badgesymbols .scaleEffect(1.0/4.0, anchor: .top) .position(x:geometry.size.width / 2.0,y:(3.0/4.0) * geometry.size.height) } } } }
パスで描く
チュートリアルより Pathで使う描画データ
struct HexagonParameters { struct Segment { let useWidth: (CGFloat, CGFloat, CGFloat) let xFactors: (CGFloat, CGFloat, CGFloat) let useHeight: (CGFloat, CGFloat, CGFloat) let yFactors: (CGFloat, CGFloat, CGFloat) } static let adjustment: CGFloat = 0.085 static let points = [ Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (0.60, 0.40, 0.50), useHeight: (1.00, 1.00, 0.00), yFactors: (0.05, 0.05, 0.00) ), Segment( useWidth: (1.00, 1.00, 0.00), xFactors: (0.05, 0.00, 0.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.20 + adjustment, 0.30 + adjustment, 0.25 + adjustment) ), Segment( useWidth: (1.00, 1.00, 0.00), xFactors: (0.00, 0.05, 0.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.70 - adjustment, 0.80 - adjustment, 0.75 - adjustment) ), Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (0.40, 0.60, 0.50), useHeight: (1.00, 1.00, 1.00), yFactors: (0.95, 0.95, 1.00) ), Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (0.95, 1.00, 1.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.80 - adjustment, 0.70 - adjustment, 0.75 - adjustment) ), Segment( useWidth: (1.00, 1.00, 1.00), xFactors: (1.00, 0.95, 1.00), useHeight: (1.00, 1.00, 1.00), yFactors: (0.30 + adjustment, 0.20 + adjustment, 0.25 + adjustment) ) ] }
ビュー
struct Badge: View { var body: some View { // GeometryReaderでビューのサイズを動的に取っている GeometryReader { geometry in // Pathの開始 Path { path in var width:CGFloat = min(geometry.size.width, geometry.size.height) let height = width let xScale: CGFloat = 0.832 let xOffset = (width * (1.0 - xScale)) / 2.0 width *= xScale // 始点にmoveする path.move(to: CGPoint(x: xOffset + width * 0.95, y: height * (0.20 + HexagonParameters.adjustment))) // ここから描画データ通りに線をおく。 HexagonParameters.points.forEach { path.addLine(to: .init(x:xOffset + width * $0.useWidth.0 * $0.xFactors.0, y:height * $0.useHeight.0 * $0.yFactors.0)) // 描画データには曲線の情報もある path.addQuadCurve(to: .init( x: xOffset + width * $0.useWidth.1 * $0.xFactors.1, y: height * $0.useHeight.1 * $0.yFactors.1), control: .init( x:xOffset + width * $0.useWidth.2 * $0.xFactors.2, y:height * $0.useHeight.2 * $0.yFactors.2)) } }// ここで描画、グラデーションをつけた。 .fill(LinearGradient(gradient: .init(colors: [Self.gradientStart, Self.gradientEnd]), startPoint: .init(x: 0.5, y: 0), endPoint: .init(x: 0.5, y: 0.6))) .aspectRatio(1, contentMode: .fit) } } static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255) static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255) }
ボタンのタップや表示のフィルター
チュートリアルより。 まず、セルにスターの表示非表示をつける。
struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50, height: 50) Text(landmark.name) Spacer() if landmark.isFavorite { Image(systemName: "star.fill") .imageScale(.medium) .foregroundColor(.yellow) } } } }
これはif文で簡単につけられる。
次にリストでお気に入りだけを表示するようにトグルをつける。
struct LandmarkList: View { @EnvironmentObject private var userData: UserData var body: some View { NavigationView { List { Toggle(isOn: $userData.showFavoritesOnly) { Text("Show Favorites Only") } ForEach(userData.landmarks) { landmark in if !self.userData.showFavoritesOnly || landmark.isFavorite { NavigationLink( destination: LandmarkDetail(landmark: landmark) .environmentObject(self.userData) ) { LandmarkRow(landmark: landmark) } } } } .navigationBarTitle(Text("Landmarks")) } } }
@EnvironmentObjectに指定しているUserDataはその属性を変化させると表示が更新される。 $userData.showFavoritesOnlyはToggle(isOn:)にバインドさせている。
詳細画面ではスターを付けられるようにしている。
struct LandmarkDetail: View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0.id == landmark.id })! } var body: some View { VStack { MapView(coordinate: landmark.locationCoordinate) .edgesIgnoringSafeArea(.top) .frame(height: 300) CircleImage(image: landmark.image) .offset(x: 0, y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { HStack { Text(landmark.name) .font(.title) Button(action: { self.userData.landmarks[self.landmarkIndex] .isFavorite.toggle() }) { if self.userData.landmarks[self.landmarkIndex] .isFavorite { Image(systemName: "star.fill") .foregroundColor(Color.yellow) } else { Image(systemName: "star") .foregroundColor(Color.gray) } } } HStack(alignment: .top) { Text(landmark.park) .font(.subheadline) Spacer() Text(landmark.state) .font(.subheadline) } } .padding() Spacer() } } } struct LandmarkDetail_Previews: PreviewProvider { static var previews: some View { let userData = UserData() return LandmarkDetail(landmark: userData.landmarks[0]) .environmentObject(userData) } }
最後にUserDataは
import Combine import SwiftUI final class UserData: ObservableObject { @Published var showFavoritesOnly = false @Published var landmarks = landmarkData }
であって、@PublishedをつけたObservableObjectであることがポイント。
リスト表示
チュートリアルより
まず静的なJSONのデータ
[ { "name": "Turtle Rock", "category": "Featured", "city": "Twentynine Palms", "state": "California", "id": 1001, "park": "Joshua Tree National Park", "coordinates": { "longitude": -116.166868, "latitude": 34.011286 }, "imageName": "turtlerock" }, {...} ]
がある。これをモデルに読み込む。モデルは
import SwiftUI import CoreLocation struct Landmark: Hashable, Codable, Identifiable { 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, Codable, Hashable { case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" } } extension Landmark { var image: Image { ImageStore.shared.image(name: imageName) } } struct Coordinates: Hashable, Codable { var latitude: Double var longitude: Double }
でCodableに対応しているのでそのまま読み込める。 読み込むところはJSONDecoderというのを用いる。
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)! } }
リストに表示するセルに対応するものは
import SwiftUI struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50, height: 50) Text(landmark.name) Spacer() } } } struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { Group { LandmarkRow(landmark: landmarkData[0]) LandmarkRow(landmark: landmarkData[1]) } .previewLayout(.fixed(width: 300, height: 70)) } }
のように簡単につくってプレビューもみることができる。 プレビューでは2つのセルを表示していてGroupでまとめてある。 これをリストに表示する。
import SwiftUI struct LandmarkList: View { var body: some View { NavigationView { List(landmarkData) {landmark in NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark:landmark) } } .navigationBarTitle(Text("Landmarks")) } } } struct LandmarkList_Previews: PreviewProvider { static var previews: some View { ForEach(["iPhone SE","iPhone XS Max"], id: \.self) { deviceName in LandmarkList() .previewDevice(PreviewDevice(rawValue: deviceName)) .previewDisplayName(deviceName) } } }
ここで、プレビューを色々なデバイスで同時にプレビューできるようにしている。 また、NavigationLinkに詳細画面に飛ばしている。その際にデータを渡している点に注目。
詳細画面は
import SwiftUI 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(x: 0, 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() } .navigationBarTitle(Text(landmark.name), displayMode: .inline) } } struct LandmarkDetail_Previews: PreviewProvider { static var previews: some View { LandmarkDetail(landmark:landmarkData[0]) } }
Viewの配置例
import SwiftUI struct ContentView: View { @State var tapped = false var tap: some Gesture { TapGesture(count: 1) .onEnded({ _ in self.tapped = !self.tapped; }) } var circle : some View { Circle() .fill(self.tapped ? Color.red : Color.black) .frame(width: 100, height: 100, alignment: .center) } var titleText : some View { Text("Hello, World") .font(.title) .foregroundColor(.green) .gesture(tap) } var body: some View { VStack { MapView() .edgesIgnoringSafeArea(.top) // ステータス部分まで描画 .frame(height:300) // MapViewは横は自動で拡がる。 CircleImage() .offset(y:-130) // ここで地図にめり込ませて .padding(.bottom, -130) // めり込んだ分を引き上げる VStack { titleText; HStack { Text("Joshua Tree National Park") .font(.headline) Spacer() Text("California") .font(.headline) } circle; } .padding() Spacer() // 一番下にバネを仕込んで上に上げる。 } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
UIKitをUIViewRepresentableで使う
makeUIViewとupdateUIViewを実装すれば良い。プレビューも効く。
import SwiftUI import MapKit struct MapView: UIViewRepresentable { func makeUIView(context:Context) -> MKMapView { MKMapView(frame: .zero) } func updateUIView(_ uiView: MKMapView, context: Context) { let coordinate = CLLocationCoordinate2D(latitude: 34.011286, longitude: -116.166868) let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0) let region = MKCoordinateRegion(center: coordinate, span: span) uiView.setRegion(region, animated: true) } }
Hello, World
まず、もっとも簡単な使い方をしてみた。
@State var tapped = false var tap: some Gesture { TapGesture(count: 1) .onEnded({ _ in self.tapped = !self.tapped; }) } var circle : some View { Circle() .fill(self.tapped ? Color.red : Color.black) .frame(width: 100, height: 100, alignment: .center) } var body: some View { VStack { Text("Hello, World") .gesture(tap) circle; } }
...argsで関数にタプルを渡す。
例えばf(S,N,B)という関数があるとして、args=["a",3,false]というタプルを渡すには f(...args)とやれば良い。同様のことは配列においても可能。
LFS
www.slideshare.net
gitで管理されるべきでないバイナリファイルなどをgit上で管理するための機能らしい。 gitはハッシュ値と保存先をリポジトリで管理している。バイナリファイルは他のストレージに保存されている。 チェックアウトする際にはコミットに含まれているバイナリファイルがストレージからとってこられる。
SourctreeでLFSは簡単にインストールして扱うことができる。