diff --git a/MyLibrary/Package.swift b/MyLibrary/Package.swift index f6f2706..dbae88a 100644 --- a/MyLibrary/Package.swift +++ b/MyLibrary/Package.swift @@ -9,7 +9,11 @@ let package = Package( products: [ .library( name: "AppFeature", - targets: ["AppFeature"]) + targets: ["AppFeature"]), + .library( + name: "GuidanceFeature", + targets: ["GuidanceFeature"] + ) ], dependencies: [ .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.9.1"), @@ -19,6 +23,7 @@ let package = Package( .target( name: "AppFeature", dependencies: [ + "GuidanceFeature", "ScheduleFeature", "SponsorFeature", "trySwiftFeature", @@ -34,6 +39,21 @@ let package = Package( .process("Resources") ] ), + .target( + name: "GuidanceFeature", + dependencies: [ + "MapKitClient", + "Safari", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + ] + ), + .target( + name: "MapKitClient", + dependencies: [ + "SharedModels", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + ] + ), .target( name: "Safari", dependencies: [ diff --git a/MyLibrary/Sources/AppFeature/AppView.swift b/MyLibrary/Sources/AppFeature/AppView.swift index f06f96e..635973d 100644 --- a/MyLibrary/Sources/AppFeature/AppView.swift +++ b/MyLibrary/Sources/AppFeature/AppView.swift @@ -1,8 +1,10 @@ import ComposableArchitecture import Foundation +import GuidanceFeature import ScheduleFeature import SponsorFeature import SwiftUI +import TipKit import trySwiftFeature @Reducer @@ -10,14 +12,20 @@ public struct AppReducer { @ObservableState public struct State: Equatable { var schedule = Schedule.State() + var guidance = Guidance.State() var sponsors = SponsorsList.State() var trySwift = TrySwift.State() - public init() {} + let mapTip: MapTip = .init() + + public init() { + try? Tips.configure([.displayFrequency(.immediate)]) + } } public enum Action { case schedule(Schedule.Action) + case guidance(Guidance.Action) case sponsors(SponsorsList.Action) case trySwift(TrySwift.Action) } @@ -28,6 +36,9 @@ public struct AppReducer { Scope(state: \.schedule, action: \.schedule) { Schedule() } + Scope(state: \.guidance, action: \.guidance) { + Guidance() + } Scope(state: \.sponsors, action: \.sponsors) { SponsorsList() } @@ -50,6 +61,11 @@ public struct AppView: View { .tabItem { Label(String(localized: "Schedule", bundle: .module), systemImage: "calendar") } + GuidanceView(store: store.scope(state: \.guidance, action: \.guidance)) + .tabItem { + Label(String(localized: "Venue", bundle: .module), systemImage: "map") + } + .popoverTip(store.mapTip) SponsorsListView(store: store.scope(state: \.sponsors, action: \.sponsors)) .tabItem { Label(String(localized: "Sponsors", bundle: .module), systemImage: "building.2") @@ -63,6 +79,14 @@ public struct AppView: View { } } +struct MapTip: Tip, Equatable { + var title: Text = Text("Go Shibuya First, NOT Garden", bundle: .module) + var message: Text? = Text( + "There are two kinds of Bellesalle in Shibuya. Learn how to get from Shibuya Station to \"Bellesalle Shibuya FIRST\". ", + bundle: .module) + var image: Image? = .init(systemName: "map.circle.fill") +} + #Preview { AppView( store: .init( diff --git a/MyLibrary/Sources/AppFeature/Localizable.xcstrings b/MyLibrary/Sources/AppFeature/Localizable.xcstrings index 4bcb3f6..c8032f1 100644 --- a/MyLibrary/Sources/AppFeature/Localizable.xcstrings +++ b/MyLibrary/Sources/AppFeature/Localizable.xcstrings @@ -11,6 +11,16 @@ } } }, + "Go Shibuya First, NOT Garden" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "渋谷ファーストです!ガーデンではありません!" + } + } + } + }, "Schedule" : { "localizations" : { "ja" : { @@ -30,6 +40,26 @@ } } } + }, + "There are two kinds of Bellesalle in Shibuya. Learn how to get from Shibuya Station to \"Bellesalle Shibuya FIRST\". " : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "渋谷にはベルサールが2つあります。渋谷駅からベルサール渋谷ファーストへの行き方を確認しましょう。" + } + } + } + }, + "Venue" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "会場" + } + } + } } }, "version" : "1.0" diff --git a/MyLibrary/Sources/GuidanceFeature/Guidance.swift b/MyLibrary/Sources/GuidanceFeature/Guidance.swift new file mode 100644 index 0000000..7e806b2 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Guidance.swift @@ -0,0 +1,318 @@ +import ComposableArchitecture +import CoreLocation +import Foundation +import MapKitClient +import MapKit +import Safari +import SwiftUI + +@Reducer +public struct Guidance { + + @ObservableState + public struct State: Equatable { + @Presents var destination: Destination.State? + var lines: Lines = .metroShibuya + var route: MKRoute? + var origin: MKMapItem? + var originTitle: LocalizedStringKey { lines.originTitle } + var destinationItem: MKMapItem? + var cameraPosition: MapCameraPosition = .automatic + var isLookAroundPresented: Bool = false + var lookAround: MKLookAroundScene? + + var routeOrigin: CLLocationCoordinate2D? { + guard let route = route else { return nil } + let pointCount = route.polyline.pointCount + var coords = [CLLocationCoordinate2D]( + repeating: kCLLocationCoordinate2DInvalid, + count: pointCount + ) + route.polyline.getCoordinates(&coords, range: NSRange(location: 0, length: pointCount)) + return coords.first + } + public init() {} + } + + public enum Action: BindableAction, ViewAction { + case binding(BindingAction) + case destination(PresentationAction) + case view(View) + case initialResponse(Result<(MKMapItem, MKMapItem, MKRoute, MKLookAroundScene?)?, Error>) + case updateResponse(Result<(MKMapItem, MKRoute, MKLookAroundScene?)?, Error>) + + public enum View { + case onAppear + case openMapTapped + } + } + + @Reducer(state: .equatable) + public enum Destination { + case safari(Safari) + } + + @Dependency(MapKitClient.self) var mapKitClient + + public init() {} + + public var body: some ReducerOf { + BindingReducer() + Reduce { state, action in + switch action { + case .view(.onAppear): + return .run { [state] send in + await send( + .initialResponse( + Result { + try await onAppear(lines: state.lines) + } + ) + ) + } + case let .initialResponse(.success(response)): + guard let response = response else { return .none } + let route = response.2 + + state.origin = response.0 + state.destinationItem = response.1 + state.route = route + state.lookAround = response.3 + //TODO: Calculate distance from 2 CLLocation + state.cameraPosition = .camera(.init(centerCoordinate: route.polyline.coordinate, distance: route.distance * 2)) + return .none + + case let .initialResponse(.failure(error)): + print(error) + return .none + + case let .updateResponse(.success(response)): + guard let response = response else { return .none } + let route = response.1 + state.origin = response.0 + state.route = route + state.lookAround = response.2 + //TODO: Calculate distance from 2 CLLocation + state.cameraPosition = .camera(.init(centerCoordinate: route.polyline.coordinate, distance: route.distance * 2)) + return .none + + case let .updateResponse(.failure(error)): + print(error) + return .none + + case .binding(\.lines): + guard let destination = state.destinationItem else { return .none } + return .run { [state] send in + await send( + .updateResponse( + Result { + try await update(with: state.lines, destination: destination) + } + ) + ) + } + + case .view(.openMapTapped): + return .run { [state] _ in + state.destinationItem?.openInMaps() + } + case .destination, .binding: + return .none + } + } + .ifLet(\.$destination, action: \.destination) + } + + func onAppear(lines: Lines) async throws -> (MKMapItem, MKMapItem, MKRoute, MKLookAroundScene?)? { + let items = try await withThrowingTaskGroup(of: (Int, MKMapItem?).self, returning: (MKMapItem?, MKMapItem?).self) { group in + group.addTask { + (0, try await mapKitClient.localSearch(lines.searchQuery, lines.region).first) + } + group.addTask { + (1, try await mapKitClient.localSearch("ベルサール渋谷ファースト", hallLocation).first) + } + var result: [Int: MKMapItem?] = [:] + for try await (index, element) in group { + result[index] = element + } + return (result[0]!, result[1]!) + } + guard let origin = items.0, let destination = items.1 else { return nil } + guard let route = try await mapKitClient.mapRoute(origin, destination) else { return nil } + let polylineOrigin = route.polyline.coords.first! + guard let geoLocation = try await mapKitClient.reverseGeocodeLocation(.init(latitude: polylineOrigin.latitude, longitude: polylineOrigin.longitude)).first else { + return nil + } + guard let lookAroundScene = try await mapKitClient.lookAround(.init(placemark: geoLocation)) else { + return (origin, destination, route, nil) + } + return (origin, destination, route, lookAroundScene) + } + + func update(with lines: Lines, destination: MKMapItem) async throws -> (MKMapItem, MKRoute, MKLookAroundScene?)? { + let origin = try await mapKitClient.localSearch(lines.searchQuery, lines.region).first + guard let origin = origin else { return nil } + guard let route = try await mapKitClient.mapRoute(origin, destination) else { + print("[Error] Route Not found", origin, destination) + return nil + } + let polylineOrigin = route.polyline.coords.first! + guard let geoLocation = try await mapKitClient.reverseGeocodeLocation(.init(latitude: polylineOrigin.latitude, longitude: polylineOrigin.longitude)).first else { + print("[Error] Reverse Geocode failed", polylineOrigin) + return nil + } + guard let lookAroundScene = try await mapKitClient.lookAround(.init(placemark: geoLocation)) else { + print("[Error] Look around scene not found", geoLocation) + return (origin, route, nil) + } + return (origin, route, lookAroundScene) + } +} + +@ViewAction(for: Guidance.self) +public struct GuidanceView: View { + + @Bindable public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + NavigationStack { + ScrollView { + warning + .padding() + picker + map + .padding() + + Button { + send(.openMapTapped) + } label: { + Text("Open Map", bundle: .module) + } + .buttonStyle(.borderedProminent) + .padding(.horizontal) + directions + venueInfo + .padding() + } + .navigationTitle(Text("Venue", bundle: .module)) + } + .lookAroundViewer(isPresented: $store.isLookAroundPresented, scene: $store.lookAround) + .onAppear { + send(.onAppear) + } + } + + @ViewBuilder + var venueInfo: some View { + VStack(alignment: .leading) { + Text("Belle Salle Shibuya First", bundle: .module) + .font(.title.bold()) + Text("Belle Salle Shibuya First address", bundle: .module) + } + } + + @ViewBuilder + var picker: some View { + Picker("Lines", selection: $store.lines) { + ForEach(Lines.allCases) { line in + Text(line.localizedKey, bundle: .module) + } + } + .pickerStyle(.segmented) + .padding(.horizontal) + } + + @ViewBuilder + var map: some View { + ZStack(alignment: .bottomLeading) { + Map(position: $store.cameraPosition) { + if let item = store.origin { + Marker(item: item) + .tint(store.lines.itemColor) + } + if let route = store.route { + if let origin = store.routeOrigin { + Marker(store.lines.exitName, coordinate: origin) + } + MapPolyline(route.polyline) + .stroke(Color.accentColor, style: .init(lineWidth: 8)) + } + if let item = store.destinationItem { + Marker(item: item) + .tint(.blue) + } + } + .mapStyle(.standard(elevation: .realistic, emphasis: .automatic, pointsOfInterest: .including([.publicTransport]), showsTraffic: false)) + .frame(minHeight: 240) + .mapControlVisibility(.hidden) + + if store.lookAround != nil { + LookAroundPreview(scene: $store.lookAround) + .frame(width: 120, height: 80, alignment: .bottomLeading) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding() + } + } + } + + @ViewBuilder + var directions: some View { + VStack(alignment: .leading) { + Text("Directions", bundle: .module) + .font(.headline) + ForEach(store.lines.directions) { direction in + VStack { + HStack { + Text("\(direction.order)") + Text(direction.description, bundle: .module) + } + .frame(maxWidth: .infinity, alignment: .leading) + Image(direction.imageName, bundle: .module) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) + } + .padding() + } + } + .padding() + + } + + @ViewBuilder + var warning: some View { + VStack(alignment: .leading) { + Label.init { + VStack(alignment: .leading) { + Text("Warning", bundle: .module) + .font(.subheadline.bold()) + .foregroundStyle(Color.accentColor) + Text("Our venue is Belle Salle Shibuya FIRST, not garden. Make sure there are two belle salle hall in Shibuya.", bundle: .module) + .font(.callout) + } + } icon: { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(Color.accentColor) + } + } + .padding() + .overlay { + RoundedRectangle(cornerRadius: 16) + .stroke(Color.accentColor, lineWidth: 1) + } + } +} + +var hallLocation: MKCoordinateRegion { + .init(center: .init(latitude: 35.657920, longitude: 139.708854), span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01)) +} + +#Preview { + GuidanceView(store: .init(initialState: .init()) { + Guidance() + }) +} diff --git a/MyLibrary/Sources/GuidanceFeature/Lines.swift b/MyLibrary/Sources/GuidanceFeature/Lines.swift new file mode 100644 index 0000000..71a5045 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Lines.swift @@ -0,0 +1,126 @@ +import Foundation +import IdentifiedCollections +import MapKit +import SwiftUI + +enum Lines: Equatable, Identifiable, CaseIterable { + var id: Self { self } + + case metroShibuya + case jrShibuya + case metroOmotesando + + var localizedKey: LocalizedStringKey { + switch self { + case .metroShibuya: + return "Metro Shibuya" + case .jrShibuya: + return "JR Shibuya" + case .metroOmotesando: + return "Omote-sando" + } + } + + var region: MKCoordinateRegion { + switch self { + case .metroShibuya: + return .init(center: .init(latitude: 35.657892, longitude: 139.703748), span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01)) + case .jrShibuya: + return .init(center: .init(latitude: 35.658575, longitude: 139.701499), span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01)) + case .metroOmotesando: + return .init(center: .init(latitude: 35.665222, longitude: 139.712543), span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01)) + } + } + + var searchQuery: String { + switch self { + case .jrShibuya: + return "JR Shibuya Station" + case .metroOmotesando: + return "表参道駅 B1" + case .metroShibuya: + return "渋谷駅 C1" + } + } + + var exitName: LocalizedStringKey { + switch self { + case .metroShibuya: + return "Exit C1" + case .jrShibuya: + return "JR Shibuya Station East Exit" + case .metroOmotesando: + return "Exit B1" + } + } + + var originTitle: LocalizedStringKey { + switch self { + case .metroShibuya: + return "Shibuya Station C1 Exit" + case .jrShibuya: + return "Shibuya Station East Exit" + case .metroOmotesando: + return "Omote-sando Station B1 Exit" + } + } + + var itemColor: Color { + switch self { + case .metroShibuya: + return Color.red + case .jrShibuya: + return Color.green + case .metroOmotesando: + return Color.purple + } + } + + var directions: IdentifiedArrayOf { + switch self { + case .metroShibuya: + return [ + .init(order: 1, description: "metro-1", imageName: "metro-1"), + .init(order: 2, description: "jr-9", imageName: "jr-9"), + .init(order: 3, description: "jr-10", imageName: "jr-10"), + .init(order: 4, description: "jr-11", imageName: "jr-11"), + ] + case .jrShibuya: + return [ + .init(order: 1, description: "jr-1", imageName: "jr-1"), + .init(order: 2, description: "jr-2", imageName: "jr-2"), + .init(order: 3, description: "jr-3", imageName: "jr-3"), + .init(order: 4, description: "jr-4", imageName: "jr-4"), + .init(order: 5, description: "jr-5", imageName: "jr-5"), + .init(order: 6, description: "jr-6", imageName: "jr-6"), + .init(order: 7, description: "jr-7", imageName: "jr-7"), + .init(order: 8, description: "jr-8", imageName: "jr-8"), + .init(order: 9, description: "jr-9", imageName: "jr-9"), + .init(order: 10, description: "jr-10", imageName: "jr-10"), + .init(order: 11, description: "jr-11", imageName: "jr-11"), + ] + case .metroOmotesando: + return [ + .init(order: 1, description: "omotesando-1", imageName: "jr-11"), + ] + } + } + + var duration: Duration { + switch self { + case .metroShibuya: + return .seconds(6 * 60) + case .jrShibuya: + return .seconds(8 * 60) + case .metroOmotesando: + return .seconds(10 * 60) + } + } + + struct Direction: Equatable, Identifiable { + var id: UUID { .init() } + var order: Int + var description: LocalizedStringKey + var imageName: String + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Localizable.xcstrings b/MyLibrary/Sources/GuidanceFeature/Localizable.xcstrings new file mode 100644 index 0000000..3d0ed7e --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Localizable.xcstrings @@ -0,0 +1,393 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "%lld" : { + + }, + "Belle Salle Shibuya First" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベルサール渋谷ファースト" + } + } + } + }, + "Belle Salle Shibuya First address" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "〒150-0011\nBelle Calle Shibuya First Tower B1 / 2F\n1-2-20, Higashi, Shibuya city, Tokyo, Japan" + } + }, + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "〒150-0011\n東京都渋谷区東1-2-20 住友不動産渋谷ファーストタワーB1・2F ベルサール渋谷ファースト" + } + } + } + }, + "Directions" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "道順" + } + } + } + }, + "Exit B1" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "B1出口" + } + } + } + }, + "Exit C1" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CI出口" + } + } + } + }, + "JR Shibuya" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "JR渋谷駅" + } + } + } + }, + "JR Shibuya Station East Exit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "JR渋谷駅東口" + } + } + } + }, + "jr-1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "After get off train, exit from Shibuya station South gate on 1st floor (Downstair), then go to East exit." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電車を降りたら、階段を降りて南改札から出てください。そのあと東口から出ます。" + } + } + } + }, + "jr-2" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "After exit from East Exit, pass through left side of downstairs." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "東口を出たら、エスカレータの左側を抜けます。" + } + } + } + }, + "jr-3" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Then, turn right at behind of pillar." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "柱の後ろで右折します。" + } + } + } + }, + "jr-4" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go straight along with bus stops." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バス停の横を直進します。" + } + } + } + }, + "jr-5" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go upstairs on the left hand." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "左手の階段を登ります。" + } + } + } + }, + "jr-6" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go diagonally right, then turn left." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "斜め右方向へ進み、そのあと左折します。" + } + } + } + }, + "jr-7" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go downstairs left of police office." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "渋谷警察署横の階段を降ります。" + } + } + } + }, + "jr-8" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go straight." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "直進します。" + } + } + } + }, + "jr-9" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keep going. You can get something to eat and drink at convenience store." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのまま直進します。途中コンビニがあるので食料を調達しても良いでしょう。" + } + } + } + }, + "jr-10" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Almost there!" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "もうすぐ到着です!" + } + } + } + }, + "jr-11" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You arrived and enter to entrance." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "到着しました。入り口からお入りください。" + } + } + } + }, + "Lines" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "路線" + } + } + } + }, + "Metro Shibuya" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "東京メトロ渋谷" + } + } + } + }, + "metro-1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "After getting off Tokyo Metro, head to C1 Exit. Then turn left" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "地下鉄を降りたらC1出口に向かいます。地上に出たら左折します。" + } + } + } + }, + "Omote-sando" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表参道駅" + } + } + } + }, + "Omote-sando Station B1 Exit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表参道駅B1出口" + } + } + } + }, + "omotesando-1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Highly recommend using map with button above." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上のボタンからマップを開くことをお勧めします。" + } + } + } + }, + "Open Map" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mapを開く" + } + } + } + }, + "Our venue is Belle Salle Shibuya FIRST, not garden. Make sure there are two belle salle hall in Shibuya." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "会場はベルサール渋谷ファーストです。渋谷ガーデンもあるのでお間違えないようにしてください。" + } + } + } + }, + "Shibuya Station C1 Exit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "渋谷駅C1出口" + } + } + } + }, + "Shibuya Station East Exit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "渋谷駅東口" + } + } + } + }, + "Venue" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "会場" + } + } + } + }, + "Warning" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "注意" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-1.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-1.imageset/Contents.json new file mode 100644 index 0000000..5f75fce --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-1.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-1.imageset/jr-1.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-1.imageset/jr-1.jpg new file mode 100644 index 0000000..da0ab15 Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-1.imageset/jr-1.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-10.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-10.imageset/Contents.json new file mode 100644 index 0000000..85c1b8c --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-10.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-10.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-10.imageset/jr-10.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-10.imageset/jr-10.jpg new file mode 100644 index 0000000..14cb02b Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-10.imageset/jr-10.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-11.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-11.imageset/Contents.json new file mode 100644 index 0000000..0e274a1 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-11.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-11.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-11.imageset/jr-11.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-11.imageset/jr-11.jpg new file mode 100644 index 0000000..106d31f Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-11.imageset/jr-11.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-2.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-2.imageset/Contents.json new file mode 100644 index 0000000..a9d8ed1 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-2.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-2.imageset/jr-2.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-2.imageset/jr-2.jpg new file mode 100644 index 0000000..e395a98 Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-2.imageset/jr-2.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-3.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-3.imageset/Contents.json new file mode 100644 index 0000000..fc6de4e --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-3.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-3.imageset/jr-3.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-3.imageset/jr-3.jpg new file mode 100644 index 0000000..008e3ee Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-3.imageset/jr-3.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-4.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-4.imageset/Contents.json new file mode 100644 index 0000000..5094ced --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-4.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-4.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-4.imageset/jr-4.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-4.imageset/jr-4.jpg new file mode 100644 index 0000000..109a230 Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-4.imageset/jr-4.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-5.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-5.imageset/Contents.json new file mode 100644 index 0000000..ced9834 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-5.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-5.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-5.imageset/jr-5.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-5.imageset/jr-5.jpg new file mode 100644 index 0000000..f576fea Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-5.imageset/jr-5.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-6.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-6.imageset/Contents.json new file mode 100644 index 0000000..c87cb95 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-6.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-6.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-6.imageset/jr-6.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-6.imageset/jr-6.jpg new file mode 100644 index 0000000..e6b2314 Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-6.imageset/jr-6.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-7.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-7.imageset/Contents.json new file mode 100644 index 0000000..3cc6f3d --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-7.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-7.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-7.imageset/jr-7.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-7.imageset/jr-7.jpg new file mode 100644 index 0000000..9ddb38d Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-7.imageset/jr-7.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-8.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-8.imageset/Contents.json new file mode 100644 index 0000000..53614a0 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-8.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-8.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-8.imageset/jr-8.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-8.imageset/jr-8.jpg new file mode 100644 index 0000000..23b67b3 Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-8.imageset/jr-8.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-9.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-9.imageset/Contents.json new file mode 100644 index 0000000..f4fb719 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-9.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "jr-9.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-9.imageset/jr-9.jpg b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-9.imageset/jr-9.jpg new file mode 100644 index 0000000..633acf6 Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/jr-9.imageset/jr-9.jpg differ diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/metro-1.imageset/Contents.json b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/metro-1.imageset/Contents.json new file mode 100644 index 0000000..f7bc1f8 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/metro-1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "metro-1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLibrary/Sources/GuidanceFeature/Media.xcassets/metro-1.imageset/metro-1.png b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/metro-1.imageset/metro-1.png new file mode 100644 index 0000000..c4b6775 Binary files /dev/null and b/MyLibrary/Sources/GuidanceFeature/Media.xcassets/metro-1.imageset/metro-1.png differ diff --git a/MyLibrary/Sources/GuidanceFeature/Util.swift b/MyLibrary/Sources/GuidanceFeature/Util.swift new file mode 100644 index 0000000..ebfee18 --- /dev/null +++ b/MyLibrary/Sources/GuidanceFeature/Util.swift @@ -0,0 +1,14 @@ +import CoreLocation +import Foundation +import MapKit + +extension MKPolyline { + var coords: [CLLocationCoordinate2D] { + var coords = [CLLocationCoordinate2D]( + repeating: kCLLocationCoordinate2DInvalid, + count: pointCount + ) + getCoordinates(&coords, range: NSRange(location: 0, length: pointCount)) + return coords + } +} diff --git a/MyLibrary/Sources/MapKitClient/Client.swift b/MyLibrary/Sources/MapKitClient/Client.swift new file mode 100644 index 0000000..b598f28 --- /dev/null +++ b/MyLibrary/Sources/MapKitClient/Client.swift @@ -0,0 +1,46 @@ +import CoreLocation +import Dependencies +import DependenciesMacros +import Foundation +import MapKit +import SharedModels + +@DependencyClient +public struct MapKitClient { + public var mapRoute: @Sendable (MKMapItem, MKMapItem) async throws -> MKRoute? + public var lookAround: @Sendable (MKMapItem) async throws -> MKLookAroundScene? + public var reverseGeocodeLocation: @Sendable (CLLocationCoordinate2D) async throws -> [MKPlacemark] + public var localSearch: @Sendable (String, MKCoordinateRegion) async throws -> [MKMapItem] +} + +extension MapKitClient: DependencyKey { + public static var liveValue: Self = .init( + mapRoute: { starting, ending in + let directionsRequest = MKDirections.Request() + directionsRequest.source = starting + directionsRequest.destination = ending + directionsRequest.transportType = .walking + + let directionsService = MKDirections(request: directionsRequest) + let response = try await directionsService.calculate() + let route = response.routes.first + return route + }, + lookAround: { mapItem in + let sceneRequest = MKLookAroundSceneRequest(mapItem: mapItem) + return try await sceneRequest.scene + }, + reverseGeocodeLocation: { location in + let geoCoder = CLGeocoder() + return try await geoCoder.reverseGeocodeLocation(.init(latitude: location.latitude, longitude: location.longitude)) + .map(MKPlacemark.init(placemark:)) + }, + localSearch: { naturalLanguageQuery, region in + let request = MKLocalSearch.Request() + request.region = region + request.naturalLanguageQuery = naturalLanguageQuery + request.resultTypes = .pointOfInterest + return try await MKLocalSearch(request: request).start().mapItems + } + ) +} diff --git a/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings b/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings index edceb1e..faa0460 100644 --- a/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings +++ b/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings @@ -370,32 +370,6 @@ } } }, - "Go Shibuya First, NOT Garden" : { - "localizations" : { - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "渋谷ガーデンではありません!渋谷ファーストに向かいましょう!" - } - } - } - }, - "Guidance URL" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "https://twitter.com/tryswiftconf/status/1108474796788977664" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "https://twitter.com/tryswiftconf/status/1108474796788977664" - } - } - } - }, "Have you ever built a socket communication app? The sense of accomplishment when you delve into layers that aren't typically touched in your everyday app development and actually utilize them in an app is truly exceptional. However, creating a socket communication app requires knowledge of POSIX sockets, and for complex communications, you need to implement parallel processing.\nIn this talk, we'll implement socket communication and parallel processing in Swift. With Swift, which we're all familiar with, you can easily venture into unfamiliar territories, and there are several instances where you can leverage Swift's capabilities through implementing socket communication and parallel processing. Take this opportunity to enjoy learning socket communication and rediscover the charm of Swift!" : { "extractionState" : "manual", "localizations" : { @@ -1055,16 +1029,6 @@ } } }, - "There are two kinds of Bellesalle in Shibuya. Learn how to get from Shibuya Station to \"Bellesalle Shibuya FIRST\". " : { - "localizations" : { - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "渋谷にはベルサールが2つあります。ベルサール渋谷ファーストへの行き方を確認しましょう。" - } - } - } - }, "This talk will take you through a handful of topics that either make a big or a small win for you or your application, mostly with the new iOS, SwiftUI and Xcode features. Want to give your app some extra sparkle? Let's chat about cool shortcuts, playful animations, and the new tricks from SwiftUI 5 like observation framework, enhanced phased animations, updates to scrollview and new gestures. Come join this session and take home some big and small wins with you." : { "extractionState" : "manual", "localizations" : { diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index 1bb74ad..c45438a 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -34,9 +34,7 @@ public struct Schedule { var workshop: Conference? @Presents var destination: Destination.State? - public init() { - try? Tips.configure([.displayFrequency(.immediate)]) - } + public init() {} } public enum Action: BindableAction, ViewAction { @@ -49,7 +47,6 @@ public struct Schedule { public enum View { case onAppear case disclosureTapped(Session) - case mapItemTapped } } @@ -59,9 +56,7 @@ public struct Schedule { } @Reducer(state: .equatable) - public enum Destination { - case guidance(Safari) - } + public enum Destination {} @Dependency(DataClient.self) var dataClient @Dependency(\.openURL) var openURL @@ -81,42 +76,34 @@ public struct Schedule { let workshop = try dataClient.fetchWorkshop() return .init(day1: day1, day2: day2, workshop: workshop) })) - case let .view(.disclosureTapped(session)): - guard let description = session.description, let speakers = session.speakers else { - return .none - } - state.path.append( - .detail( - .init( - title: session.title, - description: description, - requirements: session.requirements, - speakers: speakers + case let .view(.disclosureTapped(session)): + guard let description = session.description, let speakers = session.speakers else { + return .none + } + state.path.append( + .detail( + .init( + title: session.title, + description: description, + requirements: session.requirements, + speakers: speakers + ) ) ) - ) - return .none - case .view(.mapItemTapped): - let url = URL(string: String(localized: "Guidance URL", bundle: .module))! - #if os(iOS) || os(macOS) - state.destination = .guidance(.init(url: url)) return .none - #elseif os(visionOS) - return .run { _ in await openURL(url) } - #endif - case let .fetchResponse(.success(response)): - state.day1 = response.day1 - state.day2 = response.day2 - state.workshop = response.workshop - return .none - case let .fetchResponse(.failure(error as DecodingError)): - assertionFailure(error.localizedDescription) - return .none - case let .fetchResponse(.failure(error)): - print(error) // TODO: replace to Logger API - return .none - case .binding, .path, .destination: - return .none + case let .fetchResponse(.success(response)): + state.day1 = response.day1 + state.day2 = response.day2 + state.workshop = response.workshop + return .none + case let .fetchResponse(.failure(error as DecodingError)): + assertionFailure(error.localizedDescription) + return .none + case let .fetchResponse(.failure(error)): + print(error) // TODO: replace to Logger API + return .none + case .binding, .path, .destination: + return .none } } .forEach(\.path, action: \.path) @@ -129,8 +116,6 @@ public struct ScheduleView: View { @Bindable public var store: StoreOf - let mapTip: MapTip = .init() - public init(store: StoreOf) { self.store = store } @@ -140,17 +125,12 @@ public struct ScheduleView: View { root } destination: { store in switch store.state { - case .detail: - if let store = store.scope(state: \.detail, action: \.detail) { - ScheduleDetailView(store: store) - } + case .detail: + if let store = store.scope(state: \.detail, action: \.detail) { + ScheduleDetailView(store: store) + } } } - .sheet(item: $store.scope(state: \.destination?.guidance, action: \.destination.guidance)) { - sheetStore in - SafariViewRepresentation(url: sheetStore.url) - .ignoresSafeArea() - } } @ViewBuilder @@ -164,34 +144,24 @@ public struct ScheduleView: View { .pickerStyle(.segmented) .padding(.horizontal) switch store.selectedDay { - case .day1: - if let day1 = store.day1 { - conferenceList(conference: day1) - } else { - Text("") - } - case .day2: - if let day2 = store.day2 { - conferenceList(conference: day2) - } else { - Text("") - } - case .day3: - if let workshop = store.workshop { - conferenceList(conference: workshop) - } else { - Text("") - } - } - } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Image(systemName: "map") - .onTapGesture { - send(.mapItemTapped) + case .day1: + if let day1 = store.day1 { + conferenceList(conference: day1) + } else { + Text("") + } + case .day2: + if let day2 = store.day2 { + conferenceList(conference: day2) + } else { + Text("") + } + case .day3: + if let workshop = store.workshop { + conferenceList(conference: workshop) + } else { + Text("") } - .popoverTip(mapTip) - } } .onAppear(perform: { @@ -312,14 +282,6 @@ public struct ScheduleView: View { } } -struct MapTip: Tip, Equatable { - var title: Text = Text("Go Shibuya First, NOT Garden", bundle: .module) - var message: Text? = Text( - "There are two kinds of Bellesalle in Shibuya. Learn how to get from Shibuya Station to \"Bellesalle Shibuya FIRST\". ", - bundle: .module) - var image: Image? = .init(systemName: "map.circle.fill") -} - #Preview { ScheduleView( store: .init( diff --git a/trySwiftTokyo.xcworkspace/xcshareddata/swiftpm/Package.resolved b/trySwiftTokyo.xcworkspace/xcshareddata/swiftpm/Package.resolved index a46d368..6891b18 100644 --- a/trySwiftTokyo.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/trySwiftTokyo.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zunda-pixel/LicenseProvider", "state" : { - "revision" : "39cd5f269955fc961630f970c370f9182fe56911", - "version" : "1.1.1" + "revision" : "ca2216026c681aa154e5023bcefc560b7507268b", + "version" : "1.1.2" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "3ce83179e5f0c83ad54c305779c6b438e82aaf1d", - "version" : "1.2.1" + "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", + "version" : "1.3.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax", "state" : { - "revision" : "08a2f0a9a30e0f705f79c9cfaca1f68b71bdc775", - "version" : "510.0.0" + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" } }, {