diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 8b8cb9f..40d503c 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -11,7 +11,7 @@ jobs: name: Build and Test LuasKit scheme using watchOS simulator runs-on: macos-14 env: - scheme: 'LuasKit' + scheme: 'LuasAPI' project: 'LuasWatch.xcodeproj' platform: 'watchOS Simulator' device: 'Apple Watch SE (40mm) (2nd generation)' @@ -19,6 +19,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Setup Swift + uses: SwiftyLab/setup-swift@latest + - name: Build run: | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) diff --git a/Gemfile.lock b/Gemfile.lock index 76b6a96..df96036 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,20 +10,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.1003.0) - aws-sdk-core (3.212.0) + aws-partitions (1.1044.0) + aws-sdk-core (3.217.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.95.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (1.97.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.170.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-s3 (1.179.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -33,7 +33,7 @@ GEM commander (4.6.0) highline (~> 2.0.0) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20240107) dotenv (2.8.1) @@ -58,8 +58,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -67,8 +67,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.225.0) + fastimage (2.4.0) + fastlane (2.226.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -108,7 +108,7 @@ GEM tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) + xcpretty (~> 0.4.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.0.0) sysrandom (~> 1.0) @@ -150,12 +150,12 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.7) + http-cookie (1.0.8) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.8.1) - jwt (2.9.3) + json (2.9.1) + jwt (2.10.1) base64 mini_magick (4.13.2) mini_mime (1.1.5) @@ -166,7 +166,7 @@ GEM nkf (0.2.0) optparse (0.6.0) os (1.1.4) - plist (3.7.1) + plist (3.7.2) public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) @@ -174,10 +174,10 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.9) - rouge (2.0.7) + rexml (3.4.0) + rouge (3.28.0) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (2.4.1) security (0.1.5) signet (0.19.0) addressable (~> 2.8) @@ -209,8 +209,8 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.4.0) rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) + xcpretty (0.4.0) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) diff --git a/LuasAPI/Sources/LuasAPI/APIError.swift b/LuasAPI/Sources/LuasAPI/APIError.swift index 130290d..19d3433 100644 --- a/LuasAPI/Sources/LuasAPI/APIError.swift +++ b/LuasAPI/Sources/LuasAPI/APIError.swift @@ -2,7 +2,12 @@ // Created by Roland Gropmair on 26/12/2024. // -public enum APIError: Error { - case noTrains(String) +public enum APIError: Error, Equatable { + + /// returns a string if there was a `message` field in the API response XML + case noTrainsButMessageFromAPI(String) + + case noTrains + case invalidXML(String) } diff --git a/LuasAPI/Sources/LuasAPI/LuasAPI.swift b/LuasAPI/Sources/LuasAPI/LuasAPI.swift index ea74516..c29e649 100644 --- a/LuasAPI/Sources/LuasAPI/LuasAPI.swift +++ b/LuasAPI/Sources/LuasAPI/LuasAPI.swift @@ -39,7 +39,7 @@ public struct LuasAPI { ) } - public func getTrains(stationShortCode: String) async throws -> Data { + internal func getTrains(stationShortCode: String) async throws -> Data { let request = buildRequest(stationShortCode: stationShortCode) @@ -47,6 +47,29 @@ public struct LuasAPI { return data } + + public func dueTimes(for trainStation: TrainStation) async throws -> TrainsByDirection { + + let data = try await getTrains(stationShortCode: trainStation.shortCode) + + let trainsByDirection = try APIParser.parse(xml: data, for: trainStation) + + if trainsByDirection.inbound.isEmpty && trainsByDirection.outbound.isEmpty { + /// success - but no trains?!? + + if let messageFromAPI = trainsByDirection.message, + messageFromAPI.count > 0 { + /// if we get `message` from response XML, we return that as an error + throw APIError.noTrainsButMessageFromAPI(messageFromAPI) + + } else { + throw APIError.noTrains + } + + } else { + return trainsByDirection + } + } } public protocol URLSessionLoading { diff --git a/LuasAPI/Sources/LuasAPI/Models/TrainStation.swift b/LuasAPI/Sources/LuasAPI/Models/TrainStation.swift index 51af681..574ea7c 100644 --- a/LuasAPI/Sources/LuasAPI/Models/TrainStation.swift +++ b/LuasAPI/Sources/LuasAPI/Models/TrainStation.swift @@ -20,7 +20,7 @@ public struct TrainStation: CustomStringConvertible, Hashable, Identifiable, Sen route: Route, name: String, location: CLLocation, - stationType: TrainStation.StationType = .twoway + stationType: StationType = .twoway ) { self.stationIdShort = stationIdShort self.shortCode = shortCode @@ -30,11 +30,22 @@ public struct TrainStation: CustomStringConvertible, Hashable, Identifiable, Sen self.stationType = stationType } + // need custom implementation because location does not compare lat/long when checking for equatable + static public func == (lhs: TrainStation, rhs: TrainStation) -> Bool { + lhs.stationIdShort == rhs.stationIdShort && + lhs.shortCode == rhs.shortCode && + lhs.route == rhs.route && + lhs.name == rhs.name && + lhs.location.coordinate.latitude == rhs.location.coordinate.latitude && + lhs.location.coordinate.longitude == rhs.location.coordinate.longitude && + lhs.stationType == rhs.stationType + } + public var id: String { stationIdShort } - public enum StationType: String, Sendable { + public enum StationType: String, Sendable, Equatable { case twoway, oneway, terminal } @@ -68,4 +79,16 @@ public struct TrainStation: CustomStringConvertible, Hashable, Identifiable, Sen return formatter.string(from: distance) } + + public static var unknown: TrainStation { + TrainStation( + stationIdShort: "unknown", + shortCode: "unknown", + route: .green, + name: "Unknown", + location: CLLocation( + latitude: CLLocationDegrees(53.3163934083453), + longitude: CLLocationDegrees(-6.25344151996991)) + ) + } } diff --git a/LuasAPI/Sources/LuasAPI/Models/TrainStations.swift b/LuasAPI/Sources/LuasAPI/Models/TrainStations.swift index 7bd8560..ad688d0 100644 --- a/LuasAPI/Sources/LuasAPI/Models/TrainStations.swift +++ b/LuasAPI/Sources/LuasAPI/Models/TrainStations.swift @@ -11,7 +11,7 @@ public struct TrainStations: Sendable { // MARK: - Properties - public let stations: [TrainStation] + public let allStations: [TrainStation] // MARK: - Initializers @@ -31,11 +31,11 @@ public struct TrainStations: Sendable { fatalError("could not parse JSON file") } - self.stations = Self.trainStations(from: stationsArray) + self.allStations = Self.trainStations(from: stationsArray) } internal init(stations: [TrainStation]) { - self.stations = stations + self.allStations = stations } // MARK: - Private Methods @@ -80,26 +80,47 @@ public struct TrainStations: Sendable { // MARK: - Helpers public var redLineStations: [TrainStation] { - stations + allStations .filter { $0.route == .red } } public var greenLineStations: [TrainStation] { - stations + allStations .filter { $0.route == .green } } public func closestStation(from location: CLLocation) -> TrainStation? { - closestStation(from: location, stations: stations) + allStations.closestStation(from: location) } - public func closestStation( - from location: CLLocation, - stations: [TrainStation] - ) -> TrainStation? { + public func closestStation(from location: CLLocation, route: Route) -> TrainStation? { + switch route { + case .red: + return redLineStations.closestStation(from: location) + case .green: + return greenLineStations.closestStation(from: location) + } + } + + public func closestStationsSorted(from location: CLLocation) -> [TrainStation] { + allStations.sorted { (station1, station2) -> Bool in + station1.location.distance(from: location) < station2.location.distance(from: location) + } + } + + public func station(shortCode: String) -> TrainStation? { + allStations + .filter { $0.shortCode == shortCode } + .first + } +} + +private extension Array where Element == TrainStation { + + func closestStation(from location: CLLocation) -> TrainStation? { var closestStationSoFar: TrainStation? - stations.forEach { (station) in + self.forEach { (station) in // don't consider stations if they're too far away, currently 20km if station.location.distance(from: location) > 20000 { return @@ -116,31 +137,4 @@ public struct TrainStations: Sendable { return closestStationSoFar } - - public func closestStation(from location: CLLocation, route: Route) -> TrainStation? { - switch route { - case .red: - return closestStation(from: location, stations: redLineStations) - case .green: - return closestStation(from: location, stations: greenLineStations) - } - } - - public func station(shortCode: String) -> TrainStation? { - stations - .filter { $0.shortCode == shortCode } - .first - } - - public static var unknown: TrainStation { - TrainStation( - stationIdShort: "unknown", - shortCode: "unknown", - route: .green, - name: "Unknown", - location: CLLocation( - latitude: CLLocationDegrees(53.3163934083453), - longitude: CLLocationDegrees(-6.25344151996991)) - ) - } } diff --git a/LuasAPI/Tests/LuasAPITests/APIParserTests.swift b/LuasAPI/Tests/LuasAPITests/APIParserTests.swift index 3fe6d55..04b74bf 100644 --- a/LuasAPI/Tests/LuasAPITests/APIParserTests.swift +++ b/LuasAPI/Tests/LuasAPITests/APIParserTests.swift @@ -10,56 +10,12 @@ import Testing struct APIParserTests { - @Test func xmlAPIParser_handlesMessageNoTrainsWithApostrophe() throws { - - // Apr 2023: looks like they fixed the XML now, escaping the apostrophe, so this fix is not that urgent anymore: - // No service Stephen\'s Green - Beechwood. See news - - let apiResponse = """ - - No service Stephen’s Green – Beechwood. See news - - - - - - - - """.data(using: .utf8)! - - let trainsByDirection = try APIParser.parse( - xml: apiResponse, for: stationBluebell) - - #expect(trainsByDirection.inbound.count == 0) - #expect(trainsByDirection.outbound.count == 0) - #expect(trainsByDirection.message - == "No service Stephen’s Green – Beechwood. See news") - } - @Test func xmlAPIParser_handlesRanelaghTrains() throws { - let apiResponse = """ - - Green Line services operating normally - - - - - - - - - - - - - - - - - """.data(using: .utf8)! let trainsByDirection = try APIParser.parse( - xml: apiResponse, for: stationBluebell) + xml: APIResponseJSON.trainsRanelagh, + for: stationBluebell + ) #expect(trainsByDirection.inbound.count == 2) #expect( @@ -102,20 +58,11 @@ struct APIParserTests { } @Test func xmlAPIParser_handlesNoTraingButMessage() throws { - let apiResponse = """ - - Green Line services operating normally - - - - - - - - """.data(using: .utf8)! let trainsByDirection = try APIParser.parse( - xml: apiResponse, for: stationBluebell) + xml: APIResponseJSON.noTrainsWithMessage, + for: stationBluebell + ) #expect(trainsByDirection.inbound.count == 0) #expect(trainsByDirection.outbound.count == 0) @@ -123,20 +70,28 @@ struct APIParserTests { == "Green Line services operating normally") } + @Test func xmlAPIParser_handlesMessageNoTrainsWithApostrophe() throws { + + // Apr 2023: looks like they fixed the XML now, escaping the apostrophe, so this fix is not that urgent anymore: + // No service Stephen\'s Green - Beechwood. See news + + let trainsByDirection = try APIParser.parse( + xml: APIResponseJSON.noTrainsButMessageWithApostrophe, + for: stationBluebell + ) + + #expect(trainsByDirection.inbound.count == 0) + #expect(trainsByDirection.outbound.count == 0) + #expect(trainsByDirection.message + == "No service Stephen’s Green – Beechwood. See news") + } + @Test func xmlAPIParser_handlesNoTrainsNoMessage() throws { - let apiResponse = """ - - - - - - - - - """.data(using: .utf8)! let trainsByDirection = try APIParser.parse( - xml: apiResponse, for: stationBluebell) + xml: APIResponseJSON.noTrainsNoMessage, + for: stationBluebell + ) #expect(trainsByDirection.inbound.count == 0) #expect(trainsByDirection.outbound.count == 0) diff --git a/LuasAPI/Tests/LuasAPITests/LuasAPITests.swift b/LuasAPI/Tests/LuasAPITests/LuasAPITests.swift index c95f91e..834eabe 100644 --- a/LuasAPI/Tests/LuasAPITests/LuasAPITests.swift +++ b/LuasAPI/Tests/LuasAPITests/LuasAPITests.swift @@ -11,7 +11,7 @@ import Testing @Suite struct LuasAPITests { @Test func luasAPI_buildsAPIRequest() async throws { - let session = LuasMockSession() + let session = LuasMockSession(mockData: "someData".data(using: .utf8)!) let api = LuasAPI(session: session) let request = api.buildRequest(stationShortCode: "RAN") @@ -33,11 +33,82 @@ import Testing // #expect(data.isEmpty == false) //} - @Test func loadData_fromMockAPI() async throws { - let session = LuasMockSession() + @Test func loadData_fromMockAPI_returnsData() async throws { + let session = LuasMockSession( + mockData: APIResponseJSON.trainsRanelagh + ) let api = LuasAPI(session: session) let data = try await api.getTrains(stationShortCode: "RAN") #expect(data.isEmpty == false) } -} + + @Test func dueTimes_returnsTrains() async throws { + let session = LuasMockSession( + mockData: APIResponseJSON.trainsRanelagh + ) + let api = LuasAPI(session: session) + + let dueTimes = try await api.dueTimes(for: stationGreen) + + #expect(dueTimes.station.name == "station name 1") + #expect( + dueTimes.inbound == [ + Train( + destination: "Broombridge", + direction: "Inbound", + dueTime: "Due" + ), + Train( + destination: "Broombridge", + direction: "Inbound", + dueTime: "5" + ) + ] + ) + #expect( + dueTimes.outbound == [ + Train( + destination: "Bride's Glen", + direction: "Outbound", + dueTime: "7" + ), + Train( + destination: "Sandyford", + direction: "Outbound", + dueTime: "9" + ), + Train( + destination: "Bride's Glen", + direction: "Outbound", + dueTime: "15" + ) + ] + ) + } + + @Test func dueTimes_inboundOutboundEmptyNoMessage_throwsAPIErrorNoTrains() async throws { + let session = LuasMockSession( + mockData: APIResponseJSON.noTrainsNoMessage + ) + let api = LuasAPI(session: session) + + await #expect(performing: { + try await api.dueTimes(for: stationGreen) + }, throws: { error in + (error as? APIError) == .noTrains + }) + } + + @Test func dueTimes_inboundOutboundEmptyWithMessage_throwsAPIErrorNoTrainsWithMessage() async throws { + let session = LuasMockSession( + mockData: APIResponseJSON.noTrainsWithMessage + ) + let api = LuasAPI(session: session) + + await #expect(performing: { + try await api.dueTimes(for: stationGreen) + }, throws: { error in + (error as? APIError) == .noTrainsButMessageFromAPI("Green Line services operating normally") + }) + }} diff --git a/LuasAPI/Tests/LuasAPITests/LuasMockAPI.swift b/LuasAPI/Tests/LuasAPITests/LuasMockAPI.swift index 30be55a..06344c6 100644 --- a/LuasAPI/Tests/LuasAPITests/LuasMockAPI.swift +++ b/LuasAPI/Tests/LuasAPITests/LuasMockAPI.swift @@ -6,7 +6,10 @@ import Foundation import LuasAPI struct LuasMockSession: URLSessionLoading { + + var mockData: Data + func data(for request: URLRequest) async throws -> (Data, URLResponse) { - ("someData".data(using: .utf8)!, URLResponse()) + (mockData, URLResponse()) } } diff --git a/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationTests.swift b/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationTests.swift index b1680e7..8523063 100644 --- a/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationTests.swift +++ b/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationTests.swift @@ -99,4 +99,18 @@ struct TrainStationTests { ) #expect(station.distance(from: locationFarAway) == "10 km") } + + @Test func trainStation_unknownStation() throws { + let unknownStation = TrainStation( + stationIdShort: "unknown", + shortCode: "unknown", + route: .green, + name: "Unknown", + location: CLLocation( + latitude: CLLocationDegrees(53.3163934083453), + longitude: CLLocationDegrees(-6.25344151996991)) + ) + + #expect(unknownStation == TrainStation.unknown) + } } diff --git a/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationsTests.swift b/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationsTests.swift index 178c8b7..a91e62f 100644 --- a/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationsTests.swift +++ b/LuasAPI/Tests/LuasAPITests/ModelsTests/TrainStationsTests.swift @@ -9,21 +9,21 @@ import Testing struct TrainStationsTests { - @Test func trainStations_createsTrainStations() throws { + @Test func trainStations_initializerLoadsFromBuiltinJSON() async throws { - let stations = TrainStations(stations: [stationGreen, stationRed]) + let stations = TrainStations() - #expect(stations.greenLineStations == [stationGreen]) - #expect(stations.redLineStations == [stationRed]) + #expect(stations.allStations.count == 67) + #expect(stations.greenLineStations.count == 35) + #expect(stations.redLineStations.count == 32) } - @Test func trainStations_initializer() async throws { + @Test func trainStations_createsTrainStations() throws { - let stations = TrainStations() + let stations = TrainStations(stations: [stationGreen, stationRed]) - #expect(stations.stations.count == 67) - #expect(stations.greenLineStations.count == 35) - #expect(stations.redLineStations.count == 32) + #expect(stations.greenLineStations == [stationGreen]) + #expect(stations.redLineStations == [stationRed]) } @Test func trainStations_initializer_url() async throws { @@ -35,7 +35,7 @@ struct TrainStationsTests { let stations = TrainStations(url: bundleURL) - #expect(stations.stations.count == 67) + #expect(stations.allStations.count == 67) #expect(stations.greenLineStations.count == 35) #expect(stations.redLineStations.count == 32) } @@ -85,6 +85,47 @@ struct TrainStationsTests { route: .red) == stationRed) } + @Test func closestStations_returnsStationsRankedByDistance_redlineStations() async throws { + let stations = TrainStations() + let blueBell = stationBluebell + let blackHorse = try #require(stations.station(shortCode: "BLA")) + let kylemore = try #require(stations.station(shortCode: "KYL")) + let drimnagh = try #require(stations.station(shortCode: "DRI")) + let goldenBridge = try #require(stations.station(shortCode: "GOL")) + + let closestStations = stations.closestStationsSorted(from: blueBell.location) + + #expect(closestStations.count == 67) + #expect(closestStations.prefix(5) == [ + blueBell, + blackHorse, + kylemore, + drimnagh, + goldenBridge + ]) + } + + @Test func closestStations_returnsStationsRankedByDistance_cityCentre() async throws { + let stations = TrainStations() + let oConnellGPO = try #require(stations.station(shortCode: "OGP")) + let abbeyStreet = try #require(stations.station(shortCode: "ABB")) + let marlborough = try #require(stations.station(shortCode: "MAR")) + let westmoreLand = try #require(stations.station(shortCode: "WES")) + let oConnellUpper = try #require(stations.station(shortCode: "OUP")) + + let closestStations = stations.closestStationsSorted(from: oConnellGPO.location) + + #expect(closestStations.count == 67) + #expect(closestStations.prefix(5) == [ + oConnellGPO, + abbeyStreet, + marlborough, + westmoreLand, + oConnellUpper + ]) + #expect(closestStations.last!.shortCode == "BRI") + } + @Test func trainStations_shortcode() throws { let stations = TrainStations(stations: [stationGreen, stationRed]) diff --git a/LuasAPI/Tests/LuasAPITests/TestFixturesJSON.swift b/LuasAPI/Tests/LuasAPITests/TestFixturesJSON.swift new file mode 100644 index 0000000..3892d97 --- /dev/null +++ b/LuasAPI/Tests/LuasAPITests/TestFixturesJSON.swift @@ -0,0 +1,68 @@ +// +// Created by Roland Gropmair on 11/01/2025. +// + +import Foundation + +enum APIResponseJSON { + + static let trainsRanelagh: Data = + """ + + Green Line services operating normally + + + + + + + + + + + + + + + + + """.data(using: .utf8)! + + static let noTrainsWithMessage: Data = + """ + + Green Line services operating normally + + + + + + + + """.data(using: .utf8)! + + static let noTrainsButMessageWithApostrophe: Data = + """ + + No service Stephen’s Green – Beechwood. See news + + + + + + + + """.data(using: .utf8)! + + static let noTrainsNoMessage: Data = + """ + + + + + + + + + """.data(using: .utf8)! +} diff --git a/LuasApp/Sources/LuasApp/AppMode+Codable.swift b/LuasApp/Sources/LuasApp/AppMode+Codable.swift new file mode 100644 index 0000000..0b2aa8b --- /dev/null +++ b/LuasApp/Sources/LuasApp/AppMode+Codable.swift @@ -0,0 +1,102 @@ +// +// Created by Roland Gropmair on 05/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import Foundation +import LuasAPI + +extension AppMode: Codable { + + // https://stackoverflow.com/questions/69979095/codable-enum-with-arguments-and-fails-at-compile-time + + private enum CodingBase: String, Codable { + case closest + case closestOtherLine + case favourite // (TrainStation) + case nearby // (TrainStation) + case specific // (TrainStation) + case recents // (TrainStation) + } + + private enum CodingKeys: String, CodingKey { + case base + case stationValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let base = try container.decode(CodingBase.self, forKey: .base) + + let trainStations = TrainStations() + + switch base { + + case .closest: + self = .closest + + case .closestOtherLine: + self = .closestOtherLine + + case .favourite: + let shortCode = try container.decode(String.self, forKey: .stationValue) + if let station = trainStations.station(shortCode: shortCode) { + self = .favourite(station) + } else { + self = .closest + } + + case .nearby: + let shortCode = try container.decode(String.self, forKey: .stationValue) + if let station = trainStations.station(shortCode: shortCode) { + self = .nearby(station) + } else { + self = .closest + } + + case .specific: + let shortCode = try container.decode(String.self, forKey: .stationValue) + if let station = trainStations.station(shortCode: shortCode) { + self = .specific(station) + } else { + self = .closest + } + + case .recents: + let shortCode = try container.decode(String.self, forKey: .stationValue) + if let station = trainStations.station(shortCode: shortCode) { + self = .recents(station) + } else { + self = .closest + } + + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .closest: + try container.encode(CodingBase.closest, forKey: .base) + + case .closestOtherLine: + try container.encode(CodingBase.closestOtherLine, forKey: .base) + + case .favourite(let station): + try container.encode(CodingBase.favourite, forKey: .base) + try container.encode(station.shortCode, forKey: .stationValue) + + case .nearby(let station): + try container.encode(CodingBase.nearby, forKey: .base) + try container.encode(station.shortCode, forKey: .stationValue) + + case .specific(let station): + try container.encode(CodingBase.specific, forKey: .base) + try container.encode(station.shortCode, forKey: .stationValue) + + case .recents(let station): + try container.encode(CodingBase.recents, forKey: .base) + try container.encode(station.shortCode, forKey: .stationValue) + } + } +} diff --git a/LuasApp/Sources/LuasApp/AppModel+Codable.swift b/LuasApp/Sources/LuasApp/AppModel+Codable.swift deleted file mode 100644 index 464d9f9..0000000 --- a/LuasApp/Sources/LuasApp/AppModel+Codable.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// Created by Roland Gropmair on 05/02/2024. -// Copyright © 2024 mApps.ie. All rights reserved. -// - -import Foundation -import LuasAPI - -extension AppMode: Codable { - - // https://stackoverflow.com/questions/69979095/codable-enum-with-arguments-and-fails-at-compile-time - - private enum CodingBase: String, Codable { - case closest - case closestOtherLine - case favourite // (TrainStation) - case nearby // (TrainStation) - case specific // (TrainStation) - case recents // (TrainStation) - } - - private enum CodingKeys: String, CodingKey { - case base - case stationValue - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let base = try container.decode(CodingBase.self, forKey: .base) - - let trainStations = TrainStations() - - switch base { - - case .closest: - self = .closest - case .closestOtherLine: - self = .closestOtherLine - case .favourite: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = trainStations.station(shortCode: shortCode) { - self = .favourite(station) - } else { - self = .closest - } - case .nearby: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = trainStations.station(shortCode: shortCode) { - self = .nearby(station) - } else { - self = .closest - } - case .specific: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = trainStations.station(shortCode: shortCode) { - self = .specific(station) - } else { - self = .closest - } - case .recents: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = trainStations.station(shortCode: shortCode) { - self = .recents(station) - } else { - self = .closest - } - - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .closest: - try container.encode(CodingBase.closest, forKey: .base) - case .closestOtherLine: - try container.encode(CodingBase.closestOtherLine, forKey: .base) - case .favourite(let station): - try container.encode(CodingBase.favourite, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - case .nearby(let station): - try container.encode(CodingBase.nearby, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - case .specific(let station): - try container.encode(CodingBase.specific, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - case .recents(let station): - try container.encode(CodingBase.recents, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - } - } -} diff --git a/LuasApp/Sources/LuasApp/AppModel+CustomStringConvertible.swift b/LuasApp/Sources/LuasApp/AppModel+CustomStringConvertible.swift index 9f42b85..9991391 100644 --- a/LuasApp/Sources/LuasApp/AppModel+CustomStringConvertible.swift +++ b/LuasApp/Sources/LuasApp/AppModel+CustomStringConvertible.swift @@ -41,7 +41,7 @@ extension AppMode: CustomStringConvertible { case .favourite(let station): return "favourite: \(station.name)" case .nearby(let station): - return "nearby \(station.name)" + return "nearby: \(station.name)" case .specific(let station): return "specific: \(station.name)" case .recents(let station): diff --git a/LuasApp/Sources/LuasApp/AppModel.swift b/LuasApp/Sources/LuasApp/AppModel.swift index ab189bc..71c142d 100644 --- a/LuasApp/Sources/LuasApp/AppModel.swift +++ b/LuasApp/Sources/LuasApp/AppModel.swift @@ -85,8 +85,15 @@ public class AppModel: ObservableObject { } } - // for previews + #if DEBUG + // for SwiftUI Previews public init(_ state: AppState) { self.appState = state } + public init(_ state: AppState, + userLocation: CLLocation) { + self.appState = state + self.latestLocation = userLocation + } + #endif } diff --git a/LuasApp/Sources/LuasApp/LuasStrings.swift b/LuasApp/Sources/LuasApp/LuasStrings.swift index 401cece..2598049 100644 --- a/LuasApp/Sources/LuasApp/LuasStrings.swift +++ b/LuasApp/Sources/LuasApp/LuasStrings.swift @@ -11,11 +11,8 @@ import LuasAPI public struct LuasStrings { public static let noTrainsErrorMessage = - NSLocalizedString("Couldn’t get any trains.", comment: "") - - public static let noTrainsFallbackExplanation = NSLocalizedString( - "Either Luas is not operating, or there is a problem with the Luas website.", + "Couldn’t get any trains.", comment: "") public static let tooFarAway = @@ -46,7 +43,8 @@ public struct LuasStrings { "We are only able to find the closest station if you allow location services.\n\nPlease go to Settings -> Privacy -> Location Services to turn them on for LuasWatch.", comment: "") - public static func gettingLocationAuthError(_ errorMessage: String) -> String { + public static func gettingLocationAuthError(_ errorMessage: String) -> String + { NSLocalizedString( "Error getting your location:\n\n\(errorMessage)", comment: "") } @@ -86,7 +84,8 @@ public struct LuasStrings { NSLocalizedString( "Cannot switch directions for one-way stops", comment: "") - public static func distance(station: TrainStation, distance: String) -> String { + public static func distance(station: TrainStation, distance: String) -> String + { NSLocalizedString( "\(station.name) stop is \(distance) away", comment: diff --git a/LuasApp/Tests/LuasAppTests/AppMode+CodableTests.swift b/LuasApp/Tests/LuasAppTests/AppMode+CodableTests.swift new file mode 100644 index 0000000..f3d318e --- /dev/null +++ b/LuasApp/Tests/LuasAppTests/AppMode+CodableTests.swift @@ -0,0 +1,89 @@ +// +// Created by Roland Gropmair on 30/12/2024. +// + +import Foundation +import Testing + +@testable import LuasApp + +struct AppModeCodableTests { + + // @Test func appModeCodable_fromDecoder() throws { + // let appMode: AppMode = .closest + // + // let storedAppMode = try? JSONDecoder().decode( + // AppMode.self, from: storedAppModeData) + // } + + @Test func appModeCodable_encodeDecode_closest() throws { + let appMode: AppMode = .closest + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded == AppMode.closest) + } + + @Test func appModeCodable_encodeDecode_closestOtherLine() throws { + let appMode: AppMode = .closestOtherLine + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded == AppMode.closestOtherLine) + } + + @Test func appModeCodable_encodeDecode_favourite() throws { + let appMode: AppMode = .favourite(stationBluebell) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded.description == "favourite: Bluebell") + } + + @Test func appModeCodable_encodeDecode_favourite_notFound() throws { + let appMode: AppMode = .favourite(stationRed) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded == AppMode.closest) + } + + @Test func appModeCodable_encodeDecode_nearby() throws { + let appMode: AppMode = .nearby(stationHarcourt) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded.description == "nearby: Harcourt") + } + + @Test func appModeCodable_encodeDecode_nearby_notFound() throws { + let appMode: AppMode = .nearby(stationRed) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded == AppMode.closest) + } + + @Test func appModeCodable_encodeDecode_specific() throws { + let appMode: AppMode = .specific(stationBluebell) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded.description == "specific: Bluebell") + } + + @Test func appModeCodable_encodeDecode_specific_notFound() throws { + let appMode: AppMode = .specific(stationRed) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded == AppMode.closest) + } + + @Test func appModeCodable_encodeDecode_recents() throws { + let appMode: AppMode = .recents(stationHarcourt) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded.description == "recents: Harcourt") + } + + @Test func appModeCodable_encodeDecode_recents_notFound() throws { + let appMode: AppMode = .recents(stationRed) + let encoded = try JSONEncoder().encode(appMode) + let decoded = try JSONDecoder().decode(AppMode.self, from: encoded) + #expect(decoded == AppMode.closest) + } + +} diff --git a/LuasApp/Tests/LuasAppTests/AppModel+CodableTests.swift b/LuasApp/Tests/LuasAppTests/AppModel+CodableTests.swift deleted file mode 100644 index b66590d..0000000 --- a/LuasApp/Tests/LuasAppTests/AppModel+CodableTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Created by Roland Gropmair on 30/12/2024. -// - -import Foundation -import Testing - -@testable import LuasApp - -struct AppModelCodableTests { - - // @Test func appModelCodable_fromDecoder() throws { - // let appMode: AppMode = .closest - // - // let storedAppMode = try? JSONDecoder().decode( - // AppMode.self, from: storedAppModeData) - // } - - @Test func appModelCodable_encode() throws { - var appMode: AppMode = .closest - var encoded = try JSONEncoder().encode(appMode) - var decoded = try JSONDecoder().decode(AppMode.self, from: encoded) - #expect(decoded == AppMode.closest) - - appMode = .closestOtherLine - encoded = try JSONEncoder().encode(appMode) - decoded = try JSONDecoder().decode(AppMode.self, from: encoded) - #expect(decoded == AppMode.closestOtherLine) - - appMode = .favourite(stationBluebell) - encoded = try JSONEncoder().encode(appMode) - decoded = try JSONDecoder().decode(AppMode.self, from: encoded) - #expect(decoded == AppMode.favourite(stationBluebell)) - - appMode = .nearby(stationHarcourt) - encoded = try JSONEncoder().encode(appMode) - decoded = try JSONDecoder().decode(AppMode.self, from: encoded) - #expect(decoded == AppMode.nearby(stationHarcourt)) - } -} diff --git a/LuasKit/API/API.swift b/LuasKit/API/API.swift deleted file mode 100644 index a820861..0000000 --- a/LuasKit/API/API.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// Created by Roland Gropmair on 19/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import Foundation - -public protocol APIWorker { - func getTrains(shortCode: String) async throws -> Data -} - -public protocol API { - var apiWorker: APIWorker { get set } - - func dueTimes(for trainStation: TrainStation) async throws -> TrainsByDirection - - init(apiWorker: APIWorker) -} - -public enum APIError: Error { - case noTrains(String) - case invalidXML(String) -} - -public struct LuasAPI: API { - - public var apiWorker: APIWorker - - public init(apiWorker: APIWorker) { - self.apiWorker = apiWorker - } - - public func dueTimes(for trainStation: TrainStation) async throws -> TrainsByDirection { - - let data = try await apiWorker.getTrains(shortCode: trainStation.shortCode) - - let trainsByDirection = try APIParser.parse(xml: data, for: trainStation) - - // success - but no trains! - if trainsByDirection.inbound.isEmpty && trainsByDirection.outbound.isEmpty { - - // if we get message, we return that as an error - // otherwise we return an error: no trains - throw APIError.noTrains( - trainsByDirection.message - ?? "\(LuasStrings.noTrainsErrorMessage)\n\n\(LuasStrings.noTrainsFallbackExplanation)") - } else { - return trainsByDirection - } - } -} - -public struct RealAPIWorker: APIWorker { - - public func getTrains(shortCode: String) async throws -> Data { - - let url = URL( - string: - "https://luasforecasts.rpa.ie/xml/get.ashx?action=forecast&stop=\(shortCode)&encrypt=false")! - - let urlRequest = URLRequest( - url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 5) - - let session = URLSession.shared - - let (data, _) = try await session.data(for: urlRequest) - - return data - } - - public init() {} -} diff --git a/LuasKit/API/APIParser.swift b/LuasKit/API/APIParser.swift deleted file mode 100644 index f8e427b..0000000 --- a/LuasKit/API/APIParser.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// Created by Roland Gropmair on 19/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import Foundation - -struct APIParser { - - static let shouldLog = false - - class MessageParser: NSObject, NodeParser { - var delegateStack: ParserDelegateStack? - var result: String? - - private var message: String? - - override init() {} - - func parser(_ parser: XMLParser, foundCharacters string: String) { - if shouldLog { - myPrint("📄 \(String( describing: string))") - } - // fix: in some cases the parser calls this delegate back twice, - // e.g. with input 'No service Stephen’s Green – Beechwood. See news', - // the second time has '’s Green – Beechwood. See news' - // -> so we need to concatenate what we receive - message = (message ?? "") + string - } - - func parser( - _ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, - qualifiedName qName: String? - ) { - if shouldLog { - myPrint("📄 \(self.classForCoder) message: \(String( describing: message))") - } - result = message - delegateStack?.pop() - } - } - - class DirectionParser: NSObject, NodeParser { - var result: [Train]? = [] - var delegateStack: ParserDelegateStack? - - var direction: String - - init(direction: String) { - self.direction = direction - } - - func parser( - _ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, - qualifiedName qName: String?, - attributes attributeDict: [String: String] = [:] - ) { - if shouldLog { - myPrint("🔛 \(self.classForCoder) parsing \(elementName) attributeDict \(attributeDict)") - } - - if elementName == "tram" { - if // let destination = attributeDict["destination"], - let dueMins = attributeDict["dueMins"] { - if description != "No trams forecast" && dueMins != "" { - let train = Train( - destination: attributeDict["destination"]!, - direction: direction, dueTime: attributeDict["dueMins"]!) - result?.append(train) - } - } - } - } - - func parser( - _ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, - qualifiedName qName: String? - ) { - if elementName == "direction" { - delegateStack?.pop() - } - } - - } - - class StopInfoParser: NSObject, NodeParser { - var delegateStack: ParserDelegateStack? - var result: TrainsByDirection? - - private let trainStation: TrainStation - - private let messageParser = MessageParser() - private let directionsInboundParser = DirectionParser(direction: "Inbound") - private let directionsOutboundParser = DirectionParser(direction: "Outbound") - - init(trainStation: TrainStation) { - self.trainStation = trainStation - } - - func parser( - _ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, - qualifiedName qName: String?, attributes attributeDict: [String: String] = [:] - ) { - if shouldLog { - myPrint("\(self.classForCoder) parsing \(elementName) attributeDict \(attributeDict)") - } - - switch elementName { - case "stopInfo": - // we don't need to parse that info; we hand that in based on the API call we're making for the station - if shouldLog { myPrint("skip stopInfo") } - - case "message": - delegateStack?.push(messageParser) - - case "direction": - - if attributeDict["name"] == "Inbound" { - delegateStack?.push(directionsInboundParser) - - } else if attributeDict["name"] == "Outbound" { - delegateStack?.push(directionsOutboundParser) - } - - default: break - } - } - - func parser( - _ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, - qualifiedName qName: String? - ) { - - result = - TrainsByDirection( - trainStation: trainStation, - inbound: directionsInboundParser.result ?? [], - outbound: directionsOutboundParser.result ?? [], - message: messageParser.result) - delegateStack?.pop() - } - } - - public static func parse(xml: Data, for trainStation: TrainStation) throws -> TrainsByDirection { - let xmlParser = XMLParser(data: xml) - let delegateStack = ParserDelegateStack(xmlParser: xmlParser) - let stopInfoParser = StopInfoParser(trainStation: trainStation) - delegateStack.push(stopInfoParser) - - if xmlParser.parse() { - return stopInfoParser.result! - } else { - throw APIError.invalidXML( - "Error parsing XML: " + (xmlParser.parserError?.localizedDescription ?? "")) - } - } -} diff --git a/LuasKit/API/LuasMockAPI.swift b/LuasKit/API/LuasMockAPI.swift deleted file mode 100644 index dd409e1..0000000 --- a/LuasKit/API/LuasMockAPI.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Created by Roland Gropmair on 15/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import Foundation - -struct MockAPIWorker: APIWorker { - - enum Scenario { - case ranelaghTrains, noTrainsButMessage, noTrainsNoMessage // etc. - case serverError - case parserError - } - - // in the unit test, we can define the scenario we want to test - var scenario: Scenario = .ranelaghTrains - - // swiftlint:disable:next function_body_length - func getTrains(shortCode: String) throws -> Data { - - var xml: String - - switch scenario { - case .ranelaghTrains: - xml = """ - - Green Line services operating normally - - - - - - - - - - - - - - - - - """ - - case .noTrainsButMessage: - xml = """ - - Green Line services operating normally - - - - - - - - """ - - case .noTrainsNoMessage: - xml = """ - - - - - - - - - """ - - case .serverError: - throw NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut) - - case .parserError: - xml = "some invalid xml" - throw APIError.invalidXML(xml) - } - - return (xml as NSString).data(using: String.Encoding.utf8.rawValue)! - } -} diff --git a/LuasKit/API/Parser.swift b/LuasKit/API/Parser.swift deleted file mode 100644 index 41de0ff..0000000 --- a/LuasKit/API/Parser.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Created by Roland Gropmair on 19/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import Foundation - -// credit to BenS https://github.com/nsscreencast/425-parsing-xml-in-swift/blob/master/SwiftParsingXML/SwiftParsingXML/main.swift - -protocol ParserDelegate: XMLParserDelegate { - var delegateStack: ParserDelegateStack? { get set } - func didBecomeActive() -} - -extension ParserDelegate { - func didBecomeActive() {} -} - -protocol NodeParser: ParserDelegate { - associatedtype Item - var result: Item? { get } -} - -class ParserDelegateStack { - private var parsers: [ParserDelegate] = [] - private let xmlParser: XMLParser - - init(xmlParser: XMLParser) { - self.xmlParser = xmlParser - } - - func push(_ parser: ParserDelegate) { - parser.delegateStack = self - xmlParser.delegate = parser - parsers.append(parser) - } - - func pop() { - parsers.removeLast() - if let next = parsers.last { - xmlParser.delegate = next - next.didBecomeActive() - } else { - xmlParser.delegate = nil - } - } -} diff --git a/LuasKit/AppMode.swift b/LuasKit/AppMode.swift deleted file mode 100644 index fa1f8b4..0000000 --- a/LuasKit/AppMode.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Created by Roland Gropmair on 27/05/2024. -// Copyright © 2024 mApps.ie. All rights reserved. -// - -import Foundation - -/// The way the user decided how current station should be determined -public enum AppMode: Equatable { - - /// need location - case closest - case closestOtherLine - - /// no location required, because user selected specific station (via various options) - case favourite(TrainStation) - case nearby(TrainStation) - case specific(TrainStation) - case recents(TrainStation) - - public var specificStation: TrainStation? { - switch self { - - case .closest, .closestOtherLine: - return nil - case .favourite(let station), .nearby(let station), .specific(let station), - .recents(let station): - return station - } - } - - public var needsLocation: Bool { - self == .closest || self == .closestOtherLine - } -} diff --git a/LuasKit/AppModel+Codable.swift b/LuasKit/AppModel+Codable.swift deleted file mode 100644 index 6a132e7..0000000 --- a/LuasKit/AppModel+Codable.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Created by Roland Gropmair on 05/02/2024. -// Copyright © 2024 mApps.ie. All rights reserved. -// - -import Foundation - -extension AppMode: Codable { - - // https://stackoverflow.com/questions/69979095/codable-enum-with-arguments-and-fails-at-compile-time - - private enum CodingBase: String, Codable { - case closest - case closestOtherLine - case favourite // (TrainStation) - case nearby // (TrainStation) - case specific // (TrainStation) - case recents // (TrainStation) - } - - private enum CodingKeys: String, CodingKey { - case base - case stationValue - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let base = try container.decode(CodingBase.self, forKey: .base) - - switch base { - - case .closest: - self = .closest - case .closestOtherLine: - self = .closestOtherLine - case .favourite: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = TrainStations.sharedFromFile.station(shortCode: shortCode) { - self = .favourite(station) - } else { - self = .closest - } - case .nearby: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = TrainStations.sharedFromFile.station(shortCode: shortCode) { - self = .nearby(station) - } else { - self = .closest - } - case .specific: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = TrainStations.sharedFromFile.station(shortCode: shortCode) { - self = .specific(station) - } else { - self = .closest - } - case .recents: - let shortCode = try container.decode(String.self, forKey: .stationValue) - if let station = TrainStations.sharedFromFile.station(shortCode: shortCode) { - self = .recents(station) - } else { - self = .closest - } - - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .closest: - try container.encode(CodingBase.closest, forKey: .base) - case .closestOtherLine: - try container.encode(CodingBase.closestOtherLine, forKey: .base) - case .favourite(let station): - try container.encode(CodingBase.favourite, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - case .nearby(let station): - try container.encode(CodingBase.nearby, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - case .specific(let station): - try container.encode(CodingBase.specific, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - case .recents(let station): - try container.encode(CodingBase.recents, forKey: .base) - try container.encode(station.shortCode, forKey: .stationValue) - } - } -} diff --git a/LuasKit/AppModel+CustomStringConvertible.swift b/LuasKit/AppModel+CustomStringConvertible.swift deleted file mode 100644 index 9f42b85..0000000 --- a/LuasKit/AppModel+CustomStringConvertible.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Created by Roland Gropmair on 05/02/2024. -// Copyright © 2024 mApps.ie. All rights reserved. -// - -import Foundation - -extension AppState: CustomStringConvertible { - - public var description: String { - switch self { - - case .idle: - return "Idle" - case .gettingLocation: - return LuasStrings.gettingLocation - case .locationAuthorizationUnknown: - return LuasStrings.locationAuthorizationUnknown - case .errorGettingLocation(let errorMessage): - return errorMessage - case .errorGettingStationTooFarAway: - return LuasStrings.errorGettingStation - case .loadingDueTimes(let trainStation, _): - return LuasStrings.gettingDueTimes(trainStation) - case .errorGettingDueTimes(_, let errorMessage): - return errorMessage - case .foundDueTimes(let trains): - return LuasStrings.foundDueTimes(trains) - } - } -} - -extension AppMode: CustomStringConvertible { - - public var description: String { - switch self { - case .closest: - return "closest" - case .closestOtherLine: - return "closestOtherLine" - case .favourite(let station): - return "favourite: \(station.name)" - case .nearby(let station): - return "nearby \(station.name)" - case .specific(let station): - return "specific: \(station.name)" - case .recents(let station): - return "recents: \(station.name)" - } - } -} diff --git a/LuasKit/AppModel.swift b/LuasKit/AppModel.swift deleted file mode 100644 index 2f3f303..0000000 --- a/LuasKit/AppModel.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// Created by Roland Gropmair on 06/01/2024. -// Copyright © 2024 mApps.ie. All rights reserved. -// - -import CoreLocation -import Foundation -import SwiftUI - -// @Observable does not work - circular reference? -public class AppModel: ObservableObject { - - @Published public var appState: AppState = .idle - - @Published public var appMode: AppMode = .closest { - didSet { - do { - let encoded = try JSONEncoder().encode(appMode) - UserDefaults.standard.set(encoded, forKey: "AppMode") - - switch appMode { - - case .closest, .closestOtherLine: - self.selectedStation = nil - case .favourite(let station), .nearby(let station), - .specific(let station), .recents(let station): - self.selectedStation = station - } - NotificationCenter.default.post( - Notification(name: Notification.Name("LuasWatch.RetriggerTimer"))) - - } catch { - myPrint("error encoding appMode \(error)") - } - } - } - - // WIP do we need both - appMode & selectedStation?? - // don't need to save here, because we also set the appMode above - which gets saved - // in fact, ideally we could get rid of the selectedStation - because it's part of the appMode??!! - @Published public var selectedStation: TrainStation? - - @Published public var latestLocation: CLLocation? - - @Published public var allowStationTabviewUpdates: Bool = true - - @Published public var locationDenied: Bool = false - - public var highlightedStation: TrainStation? { - if case .specific(let specificStation) = appMode { - return specificStation - } - - return nil - } - - #if DEBUG - // so we can simulate app state in a sequence - public let mockMode = false - #endif - - public init() { - if let storedAppModeData = UserDefaults.standard.object(forKey: "AppMode") - as? Data, - let storedAppMode = try? JSONDecoder().decode( - AppMode.self, from: storedAppModeData) - { - self.appMode = storedAppMode - - switch storedAppMode { - - case .closest, .closestOtherLine: - self.selectedStation = nil - case .favourite(let station), .nearby(let station), - .specific(let station), .recents(let station): - self.selectedStation = station - // we don't need to trigger here do we? - // NotificationCenter.default.post(Notification(name: Notification.Name("LuasWatch.RetriggerTimer"))) - } - - } else { - self.appMode = .closest - self.selectedStation = nil - } - } - - // for previews - public init(_ state: AppState) { - self.appState = state - } -} diff --git a/LuasKit/AppState.swift b/LuasKit/AppState.swift deleted file mode 100644 index 9eeb0d3..0000000 --- a/LuasKit/AppState.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Created by Roland Gropmair on 27/05/2024. -// Copyright © 2024 mApps.ie. All rights reserved. -// - -import Foundation - -/// App's state machine, drives UI -public enum AppState { - - case idle - - case gettingLocation - case locationAuthorizationUnknown - case errorGettingLocation(String) - - /// when user is too far away from Dublin area - case errorGettingStationTooFarAway(String) - - // cachedTrains is optional because when we load that station for the first time, we won't have any trains cached - case loadingDueTimes(TrainStation, cachedTrains: TrainsByDirection?) - - case errorGettingDueTimes(TrainStation, String) - - case foundDueTimes(TrainsByDirection) - - public init(_ state: AppState) { - self = state - } -} - -extension AppState { - - public var isLoading: Bool { - if case .loadingDueTimes = self { - return true - } else { - return false - } - } -} diff --git a/LuasKit/CLAuthorizationStatus+Extensions.swift b/LuasKit/CLAuthorizationStatus+Extensions.swift deleted file mode 100644 index a60cf7e..0000000 --- a/LuasKit/CLAuthorizationStatus+Extensions.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Created by Roland Gropmair on 17/02/2024. -// Copyright © 2024 mApps.ie. All rights reserved. -// - -import CoreLocation - -extension CLAuthorizationStatus { - - public var readableDescription: String { - switch self { - case .notDetermined: - return "Not Determined" - case .restricted: - return "Restricted" - case .denied: - return "Denied" - case .authorizedAlways: - return "Authorized Always" - case .authorizedWhenInUse: - return "Authorized When In Use" - @unknown default: - return "Unknown" - } - } -} diff --git a/LuasKit/Info.plist b/LuasKit/Info.plist deleted file mode 100644 index c5f0e6c..0000000 --- a/LuasKit/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - 124 - - diff --git a/LuasKit/Location.swift b/LuasKit/Location.swift deleted file mode 100644 index f7990fe..0000000 --- a/LuasKit/Location.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// Created by Roland Gropmair on 05/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import CoreLocation - -public protocol LocationDelegate: AnyObject { - func didFail(_ error: LocationDelegateError) - func didEnableLocation() - func didGetLocation(_ location: CLLocation) -} - -public enum LocationDelegateError: Error { - case locationServicesNotEnabled - case locationAccessDenied - case locationManagerError(Error) -} - -enum InternalState { - case initializing, gettingLocation, stoppedUpdatingLocation, error -} - -enum LocationAuthState { - case unknown, granted, denied -} - -public class Location: NSObject { - - public weak var delegate: LocationDelegate? - - var locationAuthState: LocationAuthState = .unknown - var internalState: InternalState = .initializing - - let locationManager = CLLocationManager() - - public func promptLocationAuth() { - myPrint(#function) - locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters - locationManager.requestWhenInUseAuthorization() - } - - /// start getting location - public func start() { - myPrint("calling locationManager.startUpdatingLocation") - - internalState = .gettingLocation - locationManager.delegate = self - locationManager.startUpdatingLocation() - } - - public func update() { - if (locationAuthState == .granted - && (internalState == .stoppedUpdatingLocation || internalState == .error)) - || internalState == .initializing - || locationAuthState == .unknown - { - - myPrint( - "\(locationAuthState) \(internalState) -> calling locationManager.startUpdatingLocation") - - internalState = .gettingLocation - locationManager.delegate = self - locationManager.startUpdatingLocation() - - } else if locationAuthState == .denied { - myPrint("\(locationAuthState) \(internalState) -> calling delegate didFail(.denied)") - - delegate?.didFail(.locationAccessDenied) - - } else { - assertionFailure("internal error") - myPrint("🚨 NOT calling locationManager.startUpdatingLocation") - } - } -} - -extension Location: CLLocationManagerDelegate { - - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - myPrint("\(error)") - - internalState = .error - let nsError = error as NSError - - if nsError.domain == kCLErrorDomain && nsError.code == CLError.Code.denied.rawValue { - myPrint("didFail .locationAccessDenied") - delegate?.didFail(.locationAccessDenied) - - } else { - myPrint("didFail .locationManagerError") - delegate?.didFail(.locationManagerError(error)) - } - } - - public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - myPrint("authorizationStatus: \(manager.authorizationStatus.readableDescription)") - - switch manager.authorizationStatus { - case .notDetermined: - break - case .denied, .restricted: - locationAuthState = .denied - delegate?.didFail(.locationAccessDenied) - case .authorizedAlways, .authorizedWhenInUse: - locationAuthState = .granted - delegate?.didEnableLocation() - @unknown default: - myPrint("default") - } - } - - public func locationManager( - _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] - ) { - myPrint("\(locations)") - - guard let lastLocation = locations.last else { - assertionFailure("internal error") - myPrint("🚨 internal error: expected a location in the locations array") - return - } - - let howRecent = lastLocation.timestamp.timeIntervalSinceNow - - if abs(howRecent) < 15.0 { - - if lastLocation.horizontalAccuracy < 100 && lastLocation.verticalAccuracy < 100 { - myPrint("last location quite precise -> stopping location updates for now") - - internalState = .stoppedUpdatingLocation - /// it seems that calling stopUpdatingLocation() does still deliver sometimes 3 location updates, which causes superfluous API calls.... - /// setting the delegate to nil avoids that (but need to remember to set it to self again!) - locationManager.delegate = nil - locationManager.stopUpdatingLocation() - } - - delegate?.didGetLocation(lastLocation) - - } else { - myPrint("ignoring lastLocation because too old (\(howRecent) seconds ago") - } - } -} diff --git a/LuasKit/Logging.swift b/LuasKit/Logging.swift deleted file mode 100644 index 427ecd4..0000000 --- a/LuasKit/Logging.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Created by Roland Gropmair on 09/11/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import Foundation - -// inspired by https://gist.github.com/ccheptea/324e40dc905c961d87a62f65f7ba0462 - -public func myPrint( - _ items: Any..., separator: String = " ", terminator: String = "\n", function: String = #function, - file: String = #file, line: Int = #line -) { - - #if DEBUG - - let formatter: DateFormatter = { - let _formatter = DateFormatter() - _formatter.dateFormat = "HH:mm:ss.SSS" - return _formatter - }() - - var idx = items.startIndex - let endIdx = items.endIndex - - let dateString = formatter.string(from: NSDate.now) - - let lastSlashIndex = (file.lastIndex(of: "/") ?? String.Index(utf16Offset: 0, in: file)) - let nextIndex = file.index(after: lastSlashIndex) - let filename = file.suffix(from: nextIndex).replacingOccurrences(of: ".swift", with: "") - - let prefix = "\(dateString) \(filename).\(function):\(line)" - - repeat { - Swift.print( - "\(prefix) \(items[idx])", separator: separator, - terminator: idx == (endIdx - 1) ? terminator : separator) - idx += 1 - } while idx < endIdx - - #endif -} diff --git a/LuasKit/LuasExtensions.swift b/LuasKit/LuasExtensions.swift deleted file mode 100644 index ae762bc..0000000 --- a/LuasKit/LuasExtensions.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Created by Roland Gropmair on 17/10/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import SwiftUI - -public struct Colors { - - public static let luasRed = Color(UIColor(rgb: 0xEE4251)) - public static let luasGreen = Color(UIColor(rgb: 0x00A666)) - public static let luasPurple = Color(UIColor(rgb: 0x5235D6)) -} - -extension UIColor { - - convenience init(red: Int, green: Int, blue: Int) { - assert(red >= 0 && red <= 255, "Invalid red component") - assert(green >= 0 && green <= 255, "Invalid green component") - assert(blue >= 0 && blue <= 255, "Invalid blue component") - - self.init( - red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, - alpha: 1.0) - } - - convenience init(rgb: Int) { - self.init( - red: (rgb >> 16) & 0xFF, - green: (rgb >> 8) & 0xFF, - blue: rgb & 0xFF - ) - } -} diff --git a/LuasKit/LuasKit.h b/LuasKit/LuasKit.h deleted file mode 100644 index 97e0f2a..0000000 --- a/LuasKit/LuasKit.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// LuasKit.h -// LuasKit -// -// Created by Roland Gropmair on 19/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -#import - -//! Project version number for LuasKit. -FOUNDATION_EXPORT double LuasKitVersionNumber; - -//! Project version string for LuasKit. -FOUNDATION_EXPORT const unsigned char LuasKitVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/LuasKit/LuasStrings.swift b/LuasKit/LuasStrings.swift deleted file mode 100644 index f72a5ca..0000000 --- a/LuasKit/LuasStrings.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// Created by Roland Gropmair on 04/09/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import CoreLocation -import Foundation - -// swiftlint:disable line_length -public struct LuasStrings { - - public static let noTrainsErrorMessage = - NSLocalizedString("Couldn’t get any trains.", comment: "") - - public static let noTrainsFallbackExplanation = - NSLocalizedString( - "Either Luas is not operating, or there is a problem with the Luas website.", - comment: "") - - public static let tooFarAway = - NSLocalizedString( - "Closest Luas station is quite far away.\n\n" - + "Please try again closer to Dublin.", - comment: "") - - public static func errorGettingDueTimes(station: String) -> String { - NSLocalizedString( - "Error getting due times for station \(station)", - comment: - "Error shown when network loading failed; with placeholder for station") - } - - public static let errorNoInternet = - NSLocalizedString( - "Looks like your watch is not connected to the internet.\n\nPlease check your internet connection and try again.", - comment: "") - - public static let locationServicesDisabled = - NSLocalizedString( - "Error getting your location:\n\nLocation Services not enabled", - comment: "") - - public static let locationAccessDenied = - NSLocalizedString( - "We are only able to find the closest station if you allow location services.\n\nPlease go to Settings -> Privacy -> Location Services to turn them on for LuasWatch.", - comment: "") - - public static func gettingLocationAuthError(_ errorMessage: String) -> String { - NSLocalizedString( - "Error getting your location:\n\n\(errorMessage)", comment: "") - } - - public static let locationAuthorizationUnknown = - NSLocalizedString( - "Please grant location access so LuasWatch can find the closest LUAS stop.", - comment: "") - - public static let gettingLocation = - NSLocalizedString("Getting location...", comment: "") - - public static let gettingLocationOtherError = - NSLocalizedString( - "Error getting your location:\n\nOther error", comment: "") - - public static let errorGettingStation = - NSLocalizedString( - "Error finding station.\n\nPlease try again later.", comment: "") - - public static func gettingDueTimes(_ trainStation: TrainStation) -> String { - NSLocalizedString("Getting times for \(trainStation.name)...", comment: "") - } - - public static func foundDueTimes(_ trains: TrainsByDirection) -> String { - NSLocalizedString("Found times: \(trains)", comment: "") - } - - public static let loadingDueTimes = NSLocalizedString( - "Loading...", comment: "Shown while loading data from internet") - - public static let switchingDirectionsNotAllowedForFinalStop = - NSLocalizedString( - "Cannot switch directions for final stops", comment: "") - - public static let switchingDirectionsNotAllowedForOnewayStop = - NSLocalizedString( - "Cannot switch directions for one-way stops", comment: "") - - public static func distance(station: TrainStation, distance: String) -> String { - NSLocalizedString( - "\(station.name) stop is \(distance) away", - comment: - "String indicating distance of this Luas station (in meters) - input1: station name; input2: distance string" - ) - } - - public static let noTrains = NSLocalizedString( - "No trains due", - comment: "String shown when no trains in specified direction") - - public static let trainsLoading = NSLocalizedString( - "Loading trains...", - comment: "String shown when trains are loading for station") - - public static let locationDeniedFooter = NSLocalizedString( - "Unable to determine closest station, because location access not granted or disabled.\nYou can still select a station manually.", - comment: - "Label shown when location access was denied by user, to explain why nearby stations won't work" - ) -} -// swiftlint:enable line_length diff --git a/LuasKit/Models/Direction.swift b/LuasKit/Models/Direction.swift deleted file mode 100644 index 86cb111..0000000 --- a/LuasKit/Models/Direction.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// Created by Roland Gropmair on 07.07.20. -// Copyright © 2020 mApps.ie. All rights reserved. -// - -import AppIntents -import Foundation - -public enum Direction: Int, CaseIterable, Codable { - - case both, inbound, outbound -} - -@available(iOSApplicationExtension 16.0, *) -extension Direction: AppEnum { - - static var typeDisplayName: LocalizedStringResource = "Direction" - - public static var typeDisplayRepresentation = TypeDisplayRepresentation( - name: "Direction of the train (for stations that have both)") - - public static var caseDisplayRepresentations: [Direction: DisplayRepresentation] { - [.inbound: "inbound trains", .outbound: "outbound trains", .both: "both directions"] - } -} - -extension Direction: CustomStringConvertible { - - public var description: String { - return text() - } - - public func text() -> String { - switch self { - case .both: - return "Both directions" - case .inbound: - return "Inbound" - case .outbound: - return "Outbound" - } - } - - public func next() -> Direction { - switch self { - case .both: - return .inbound - case .inbound: - return .outbound - case .outbound: - return .both - } - } -} - -extension Direction { - - fileprivate static let userDefaultsKey = "DirectionStates" - - public static func direction(for station: String) -> Direction { - let userDefaults = UserDefaults.standard - - if let directions = userDefaults.object(forKey: userDefaultsKey) as? [String: Int], - let direction = directions[station] - { - return Direction(rawValue: direction)! - } - - // haven't found a value for this station: fallback default is `.both` - return .both - } - - public static func setDirection(for station: String, to direction: Direction) { - let userDefaults = UserDefaults.standard - - if var directions = userDefaults.object(forKey: userDefaultsKey) as? [String: Int] { - directions[station] = direction.rawValue - userDefaults.set(directions, forKey: userDefaultsKey) - myPrint("updating directions \(directions)") - } else { - // first time we set anything: start from scratch with dictionary with only one entry - let direction: [String: Int] = [station: direction.rawValue] - userDefaults.set(direction, forKey: userDefaultsKey) - myPrint("setting direction \(direction)") - } - } -} diff --git a/LuasKit/Models/Route.swift b/LuasKit/Models/Route.swift deleted file mode 100644 index 2d0cfa0..0000000 --- a/LuasKit/Models/Route.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Created by Roland Gropmair on 23/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import Foundation - -typealias JSONDictionary = [String: Any] - -public enum Route: Int, Codable { - case red, green -} - -extension Route { - init?(_ routeString: String) { - if routeString == "Red" { - self = .red - } else if routeString == "Green" { - self = .green - } else { - return nil - } - } - - public var other: Route { - switch self { - case .red: - return .green - case .green: - return .red - } - } -} diff --git a/LuasKit/Models/Train.swift b/LuasKit/Models/Train.swift deleted file mode 100644 index 828385a..0000000 --- a/LuasKit/Models/Train.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Created by Roland Gropmair on 23/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import Foundation - -public struct Train: CustomStringConvertible, Hashable, Codable { - - public var id: String { - UUID().uuidString - } - - public let destination: String - public let direction: String - public let dueTime: String - - public var description: String { - "\(destination.replacingOccurrences(of: "LUAS ", with: "")): \'\(dueTimeDescription)\'" - } - - public var dueTimeDescription: String { - "\(destination.replacingOccurrences(of: "LUAS ", with: "")): " - + ((dueTime.lowercased() == "due") ? "Due" : "\(dueTime) mins") - } - - public var destinationDescription: String { - destination.replacingOccurrences(of: "LUAS ", with: "") - } - - public var dueTimeDescription2: String { - (dueTime.lowercased() == "due") ? "Due" : dueTime - } - - public var destinationDueTimeDescription: String { - "Luas to \(destination) \(dueTime.lowercased() == "due" ? "is Due" : "in \(dueTime)")" - } - - public init(destination: String, direction: String, dueTime: String) { - self.destination = destination - self.direction = direction - self.dueTime = dueTime - } -} diff --git a/LuasKit/Models/TrainStation.swift b/LuasKit/Models/TrainStation.swift deleted file mode 100644 index a745f81..0000000 --- a/LuasKit/Models/TrainStation.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Created by Roland Gropmair on 23/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import CoreLocation -import Foundation - -public struct TrainStation: CustomStringConvertible, Hashable, Identifiable { - - public var id: String { - stationId - } - - public enum StationType: String { - case twoway, oneway, terminal - } - - public let stationId: String // not sure what that 'id' is for? - public let stationIdShort: String // that is the 'id' required for the API - public let shortCode: String // three-letter code, such as 'RAN'; for the XML API - public let route: Route - public let name: String - public let location: CLLocation - public let stationType: StationType - - public var description: String { - return - "\n<\(stationIdShort)> \(name) (\(location.coordinate.latitude)/\(location.coordinate.longitude)) type \(stationType)" - } - - public init( - stationId: String, stationIdShort: String, shortCode: String, - route: Route, name: String, - location: CLLocation, stationType: StationType = .twoway - ) { - self.stationId = stationId - self.stationIdShort = stationIdShort - self.shortCode = shortCode - self.route = route - self.name = name - self.location = location - self.stationType = stationType - } - - public var isFinalStop: Bool { - stationType == .terminal - } - - public var isOneWayStop: Bool { - stationType == .oneway - } - - public var allowsSwitchingDirection: Bool { - stationType == .twoway - } - - // will return nil if the distance is quite small, i.e. if the user is quite close to the station - public func distance(from userLocation: CLLocation) -> String? { - let minimumDistance = Measurement(value: 200, unit: .meters) - let distance = Measurement( - value: location.distance(from: userLocation), - unit: .meters) - - guard distance > minimumDistance else { return nil } - - return Self.distanceFormatter.string(from: distance) - } - - private static let distanceFormatter: MeasurementFormatter = { - let formatter = MeasurementFormatter() - formatter.locale = Locale(identifier: "en_IE") // not correct we hard coded the locale here! - formatter.unitOptions = .naturalScale - formatter.unitStyle = .medium - formatter.numberFormatter.usesSignificantDigits = true - formatter.numberFormatter.maximumSignificantDigits = 1 - - return formatter - }() -} diff --git a/LuasKit/Models/TrainStations.swift b/LuasKit/Models/TrainStations.swift deleted file mode 100644 index a8da612..0000000 --- a/LuasKit/Models/TrainStations.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// Created by Roland Gropmair on 23/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import CoreLocation -import Foundation - -public struct TrainStations { - - public let stations: [TrainStation] - - public static let sharedFromFile = Self.fromFile() - - private static func fromFile() -> TrainStations { - TrainStations(fromFile: "luasStops") - } - - fileprivate static func trainStations(from stationsArray: [JSONDictionary]) -> [TrainStation] { - // swiftlint:disable force_cast - stationsArray.compactMap { (station) in - - var stationTypeValue: TrainStation.StationType = .twoway - - if let stationTypeString = station["type"] as? String, - let stationType = TrainStation.StationType(rawValue: stationTypeString) - { - stationTypeValue = stationType - } - - return TrainStation( - stationId: station["stationId"] as! String, - stationIdShort: station["stationIdShort"] as! String, - shortCode: station["shortCode"] as! String, - route: Route(station["route"] as! String)!, - name: station["name"] as! String, - location: CLLocation( - latitude: CLLocationDegrees(station["lat"] as! Double), - longitude: CLLocationDegrees(station["long"] as! Double)), - stationType: stationTypeValue) - } - // swiftlint:enable force_cast - } - - private init(fromFile fileName: String) { - let identifier = "ie.mapps.LuasKit" - - guard - let luasStopsFile = Bundle(identifier: identifier)? - .url(forResource: fileName, withExtension: "json"), - let data = try? Data(contentsOf: luasStopsFile), - let json = try? JSONSerialization.jsonObject(with: data, options: []) as? JSONDictionary, - let stationsArray = json["stations"] as? [JSONDictionary] - else { fatalError("could not parse JSON file") } - - self.stations = Self.trainStations(from: stationsArray) - } - - public init(stations: [TrainStation]) { - self.stations = stations - } - - public var redLineStations: [TrainStation] { - stations - .filter { $0.route == .red } - } - - public var greenLineStations: [TrainStation] { - stations - .filter { $0.route == .green } - } - - public func closestStation(from location: CLLocation) -> TrainStation? { - closestStation(from: location, stations: stations) - } - - public func closestStation( - from location: CLLocation, - stations: [TrainStation] - ) -> TrainStation? { - var closestStationSoFar: TrainStation? - - stations.forEach { (station) in - // don't consider stations if they're too far away, currently 20km - if station.location.distance(from: location) > 20000 { - return - } - - if let closest = closestStationSoFar { - if station.location.distance(from: location) < closest.location.distance(from: location) { - closestStationSoFar = station - } - } else { - closestStationSoFar = station - } - } - - return closestStationSoFar - } - - public func closestStation(from location: CLLocation, route: Route) -> TrainStation? { - switch route { - case .red: - return closestStation(from: location, stations: redLineStations) - case .green: - return closestStation(from: location, stations: greenLineStations) - } - } - - public func station(shortCode: String) -> TrainStation? { - stations - .filter { $0.shortCode == shortCode } - .first - } - - public static var unknown: TrainStation { - TrainStation( - stationId: "id", - stationIdShort: "unknown", - shortCode: "unknown", - route: .green, - name: "Unknown", - location: CLLocation( - latitude: CLLocationDegrees(53.3163934083453), - longitude: CLLocationDegrees(-6.25344151996991))) - } -} diff --git a/LuasKit/Models/TrainsByDirection.swift b/LuasKit/Models/TrainsByDirection.swift deleted file mode 100644 index e3bd2c2..0000000 --- a/LuasKit/Models/TrainsByDirection.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Created by Roland Gropmair on 11/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import Foundation - -public struct TrainsByDirection { - public let trainStation: TrainStation - - public let inbound: [Train] - public let outbound: [Train] - public let message: String? // XML api gives message - - public init( - trainStation: TrainStation, - inbound: [Train], - outbound: [Train], - message: String? = nil - ) { - self.trainStation = trainStation - self.inbound = inbound - self.outbound = outbound - self.message = message - } - - public func shortcutOutput(direction: Direction) -> String { - var output = "" - - if direction == .inbound || direction == .both { - output += - inbound - .compactMap { $0.destinationDueTimeDescription + ".\n" } - .joined() - } - - if direction == .outbound || direction == .both { - output += - outbound - .compactMap { $0.destinationDueTimeDescription + ".\n" } - .joined() - } - - return output.count > 0 ? output : "No trains found for \(trainStation.name) LUAS stop.\n" - } - - private static let cutoffSmall = 3 - private static let cutoffLarge = 6 - - public var inboundHasOverflowSmall: Bool { - inbound.count > Self.cutoffSmall - } - - public var outboundHasOverflowSmall: Bool { - outbound.count > Self.cutoffSmall - } - - public var inboundHasOverflowLarge: Bool { - inbound.count > Self.cutoffLarge - } - - public var outboundHasOverflowLarge: Bool { - outbound.count > Self.cutoffLarge - } - - public var inboundNoOverflowSmall: [Train] { - inboundHasOverflowSmall ? Array(inbound.prefix(upTo: Self.cutoffSmall)) : inbound - } - - public var outboundNoOverflowSmall: [Train] { - outboundHasOverflowSmall ? Array(outbound.prefix(upTo: Self.cutoffSmall)) : outbound - } - - public var inboundNoOverflowLarge: [Train] { - inboundHasOverflowLarge ? Array(inbound.prefix(upTo: Self.cutoffLarge)) : inbound - } - - public var outboundNoOverflowLarge: [Train] { - outboundHasOverflowLarge ? Array(outbound.prefix(upTo: Self.cutoffLarge)) : outbound - } -} diff --git a/LuasKit/MyUserDefaults.swift b/LuasKit/MyUserDefaults.swift deleted file mode 100644 index 6f708d1..0000000 --- a/LuasKit/MyUserDefaults.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Created by Roland Gropmair on 06.08.20. -// Copyright © 2020 mApps.ie. All rights reserved. -// - -import Foundation - -public struct MyUserDefaults { - fileprivate static let userDefaultsKeySelectedStation = "SelectedStationIdShort" - - public static func userSelectedSpecificStation() -> TrainStation? { - guard let stationId = UserDefaults.standard.string(forKey: userDefaultsKeySelectedStation), - let station = TrainStations.sharedFromFile.stations.filter({ $0.stationIdShort == stationId }) - .first - else { return nil } - - return station - } - - public static func saveSelectedStation(_ station: TrainStation) { - UserDefaults.standard.set(station.stationIdShort, forKey: userDefaultsKeySelectedStation) - } - - public static func wipeUserSelectedStation() { - UserDefaults.standard.removeObject(forKey: userDefaultsKeySelectedStation) - } -} diff --git a/LuasKit/luasStops.json b/LuasKit/luasStops.json deleted file mode 100644 index 6ac6614..0000000 --- a/LuasKit/luasStops.json +++ /dev/null @@ -1,77 +0,0 @@ -{"stations": [ - {"stationIdShort": "LUAS53", "stationId": "823GA00418", "shortCode": "SAG", "route": "Red", "name": "Saggart", "lat": 53.2847775062216, "long": -6.43773921457577, "type": "terminal"}, - {"stationIdShort": "LUAS52", "stationId": "823GA00415", "shortCode": "FOR", "route": "Red", "name": "Fortunestown", "lat": 53.2842559530261, "long": -6.42474013039955}, - {"stationIdShort": "LUAS51", "stationId": "823GA00412", "shortCode": "CIT", "route": "Red", "name": "Citywest Campus", "lat": 53.2877997570853, "long": -6.41882011133547}, - {"stationIdShort": "LUAS50", "stationId": "823GA00395", "shortCode": "CVN", "route": "Red", "name": "Cheeverstown", "lat": 53.2910061402436, "long": -6.4064915096011}, - {"stationIdShort": "LUAS49", "stationId": "823GA00392", "shortCode": "FET", "route": "Red", "name": "Fettercairn", "lat": 53.2933175201804, "long": -6.39587472807755}, - - {"stationIdShort": "LUAS1", "stationId": "823GA00344", "shortCode": "TAL", "route": "Red", "name": "Tallaght", "lat": 53.2874574706805, "long": -6.37464564744776, "type": "terminal"}, - {"stationIdShort": "LUAS2", "stationId": "823GA00341", "shortCode": "HOS", "route": "Red", "name": "Hospital", "lat": 53.2893482491041, "long": -6.37884879054193}, - {"stationIdShort": "LUAS3", "stationId": "823GA00338", "shortCode": "COO", "route": "Red", "name": "Cookstown", "lat": 53.2932931595014, "long": -6.38408479509437}, - - {"stationIdShort": "LUAS4", "stationId": "823GA00347", "shortCode": "BEL", "route": "Red", "name": "Belgard", "lat": 53.2992453188537, "long": -6.37497819204135}, - {"stationIdShort": "LUAS5", "stationId": "823GA00350", "shortCode": "KIN", "route": "Red", "name": "Kingswood", "lat": 53.3036450480884, "long": -6.36544815713673}, - {"stationIdShort": "LUAS6", "stationId": "823GA00353", "shortCode": "RED", "route": "Red", "name": "Red Cow", "lat": 53.3167459644448, "long": -6.36978055275248}, - {"stationIdShort": "LUAS7", "stationId": "822GA00356", "shortCode": "KYL", "route": "Red", "name": "Kylemore", "lat": 53.3265889562716, "long": -6.34376337103717}, - {"stationIdShort": "LUAS8", "stationId": "822GA00360", "shortCode": "BLU", "route": "Red", "name": "Bluebell", "lat": 53.3292817872831, "long": -6.33382500275916}, - {"stationIdShort": "LUAS9", "stationId": "822GA00363", "shortCode": "BLA", "route": "Red", "name": "Blackhorse", "lat": 53.3342630170598, "long": -6.32753451321294}, - {"stationIdShort": "LUAS10", "stationId": "822GA00366", "shortCode": "DRI", "route": "Red", "name": "Drimnagh", "lat": 53.3353461945075, "long": -6.31827231820978}, - {"stationIdShort": "LUAS11", "stationId": "822GA00369", "shortCode": "GOL", "route": "Red", "name": "Goldenbridge", "lat": 53.335914285121, "long": -6.31330963762211}, - {"stationIdShort": "LUAS12", "stationId": "822GA00372", "shortCode": "SUI", "route": "Red", "name": "Suir Road", "lat": 53.3365921771526, "long": -6.30723125795765}, - {"stationIdShort": "LUAS13", "stationId": "822GA00375", "shortCode": "RIA", "route": "Red", "name": "Rialto", "lat": 53.3378991889509, "long": -6.29738845667397}, - {"stationIdShort": "LUAS14", "stationId": "822GA00378", "shortCode": "FAT", "route": "Red", "name": "Fatima", "lat": 53.338417448678, "long": -6.29277257068148}, - {"stationIdShort": "LUAS15", "stationId": "822GA00381", "shortCode": "JAM", "route": "Red", "name": "James's", "lat": 53.3418126087839, "long": -6.29323766222323}, - {"stationIdShort": "LUAS16", "stationId": "822GA00386", "shortCode": "HEU", "route": "Red", "name": "Heuston", "lat": 53.3466812805465, "long": -6.29178148979066}, - {"stationIdShort": "LUAS17", "stationId": "822GA00389", "shortCode": "MUS", "route": "Red", "name": "Museum", "lat": 53.3478792135209, "long": -6.2869420647211}, - {"stationIdShort": "LUAS18", "stationId": "822GA00398", "shortCode": "SMI", "route": "Red", "name": "Smithfield", "lat": 53.3471135023683, "long": -6.27808080493658}, - {"stationIdShort": "LUAS19", "stationId": "822GA00401", "shortCode": "FOU", "route": "Red", "name": "Four Courts", "lat": 53.3468518865013, "long": -6.27366041080432}, - {"stationIdShort": "LUAS20", "stationId": "822GA00404", "shortCode": "JER", "route": "Red", "name": "Jervis", "lat": 53.3476830068047, "long": -6.26527546402985}, - {"stationIdShort": "LUAS21", "stationId": "822GA00408", "shortCode": "ABB", "route": "Red", "name": "Abbey Street", "lat": 53.3485637203993, "long": -6.2584803582757}, - {"stationIdShort": "LUAS22", "stationId": "822GA00420", "shortCode": "BUS", "route": "Red", "name": "Busáras", "lat": 53.3501166497177, "long": -6.25158234663218}, - - {"stationIdShort": "LUAS23", "stationId": "822GA00423", "shortCode": "CON", "route": "Red", "name": "Connolly", "lat": 53.3509834907561, "long": -6.25001465932888, "type": "terminal"}, - - {"stationIdShort": "LUAS54", "stationId": "822GA00427", "shortCode": "GDK", "route": "Red", "name": "George's Dock", "lat": 53.3494286142281, "long": -6.24756983199852}, - {"stationIdShort": "LUAS55", "stationId": "822GA00430", "shortCode": "MYS", "route": "Red", "name": "Mayor Square - NCI", "lat": 53.3491676359234, "long": -6.24326950689034}, - {"stationIdShort": "LUAS56", "stationId": "822GA00433", "shortCode": "SDK", "route": "Red", "name": "Spencer Dock", "lat": 53.3487804260116, "long": -6.23712686504906}, - {"stationIdShort": "LUAS57", "stationId": "822GA00436", "shortCode": "TPT", "route": "Red", "name": "The Point", "lat": 53.3482763518136, "long": -6.22918667965209, "type": "terminal"}, - - {"stationIdShort": "LUAS71", "stationId": "gen:57102:3588:1", "shortCode": "BRO", "route": "Green", "name": "Broombridge", "lat": 53.3722378096567, "long": -6.2976872701795, "type": "terminal"}, - {"stationIdShort": "LUAS70", "stationId": "gen:57102:3587:1", "shortCode": "CAB", "route": "Green", "name": "Cabra", "lat": 53.3643367750274, "long": -6.28196929164465}, - {"stationIdShort": "LUAS69", "stationId": "gen:57102:3586:1", "shortCode": "PHI", "route": "Green", "name": "Phibsborough", "lat": 53.3603735102972, "long": -6.2788833745237}, - {"stationIdShort": "LUAS68", "stationId": "gen:57102:3585:1", "shortCode": "GRA", "route": "Green", "name": "Grangegorman", "lat": 53.3571964802048, "long": -6.27734377414817}, - {"stationIdShort": "LUAS67", "stationId": "gen:57102:3584:1", "shortCode": "BRD", "route": "Green", "name": "Broadstone - DIT", "lat": 53.3540707527996, "long": -6.27375924291229}, - {"stationIdShort": "LUAS66", "stationId": "gen:57102:3577:1", "shortCode": "DOM", "route": "Green", "name": "Dominick", "lat": 53.3513451173962, "long": -6.265547356884}, - - {"stationIdShort": "LUAS65", "stationId": "gen:57102:3573:1", "shortCode": "PAR", "route": "Green", "name": "Parnell", "lat": 53.3531052634032, "long": -6.26050348365009, "type": "oneway"}, - {"stationIdShort": "LUAS62", "stationId": "gen:57102:3575:1", "shortCode": "MAR", "route": "Green", "name": "Marlborough", "lat": 53.3492448734525, "long": -6.25773158174389, "type": "oneway"}, - {"stationIdShort": "LUAS60", "stationId": "gen:57102:3576:1", "shortCode": "TRY", "route": "Green", "name": "Trinity Luas Stop", "lat": 53.3452797501246, "long": -6.25825374630148, "type": "oneway"}, - - {"stationIdShort": "LUAS64", "stationId": "gen:57102:3571:1", "shortCode": "OUP", "route": "Green", "name": "O'Connell Upper", "lat": 53.3516119242553, "long": -6.26102997364648, "type": "oneway"}, - {"stationIdShort": "LUAS63", "stationId": "gen:57102:3570:1", "shortCode": "OGP", "route": "Green", "name": "O'Connell - GPO", "lat": 53.3488448095802, "long": -6.25988086113834, "type": "oneway"}, - {"stationIdShort": "LUAS61", "stationId": "gen:57102:3569:1", "shortCode": "WES", "route": "Green", "name": "Westmoreland", "lat": 53.3463598738878, "long": -6.25897573265942, "type": "oneway"}, - - {"stationIdShort": "LUAS59", "stationId": "gen:57102:3567:1", "shortCode": "DAW", "route": "Green", "name": "Dawson", "lat": 53.3421747760092, "long": -6.25797481079348}, - {"stationIdShort": "LUAS24", "stationId": "822GA00058", "shortCode": "STS", "route": "Green", "name": "St.Stephen's Green", "lat": 53.339128708973, "long": -6.26111748798404}, - {"stationIdShort": "LUAS25", "stationId": "822GA00440", "shortCode": "HAR", "route": "Green", "name": "Harcourt", "lat": 53.3336246192981, "long": -6.26273785213714}, - {"stationIdShort": "LUAS26", "stationId": "822GA00070", "shortCode": "CHA", "route": "Green", "name": "Charlemont", "lat": 53.3306152452483, "long": -6.25853598870766}, - {"stationIdShort": "LUAS27", "stationId": "822GA00074", "shortCode": "RAN", "route": "Green", "name": "Ranelagh", "lat": 53.326139822635, "long": -6.25612105038534}, - {"stationIdShort": "LUAS28", "stationId": "822GA00083", "shortCode": "BEE", "route": "Green", "name": "Beechwood", "lat": 53.3209234117233, "long": -6.25466755130786}, - {"stationIdShort": "LUAS29", "stationId": "822GA00275", "shortCode": "COW", "route": "Green", "name": "Cowper", "lat": 53.3163934083453, "long": -6.25344151996891}, - {"stationIdShort": "LUAS30", "stationId": "822GA00278", "shortCode": "MIL", "route": "Green", "name": "Milltown", "lat": 53.3096724384224, "long": -6.25174998445441}, - {"stationIdShort": "LUAS31", "stationId": "825GA00281", "shortCode": "WIN", "route": "Green", "name": "Windy Arbour", "lat": 53.3017475638249, "long": -6.2507083607334}, - {"stationIdShort": "LUAS32", "stationId": "825GA00286", "shortCode": "DUN", "route": "Green", "name": "Dundrum", "lat": 53.2923905211743, "long": -6.24518032276029}, - {"stationIdShort": "LUAS33", "stationId": "825GA00291", "shortCode": "BAL", "route": "Green", "name": "Balally", "lat": 53.2860547121413, "long": -6.23671065468435}, - {"stationIdShort": "LUAS35", "stationId": "825GA00295", "shortCode": "KIL", "route": "Green", "name": "Kilmacud", "lat": 53.2829105207139, "long": -6.22410700107201}, - {"stationIdShort": "LUAS36", "stationId": "825GA00297", "shortCode": "STI", "route": "Green", "name": "Stillorgan", "lat": 53.2793614400627, "long": -6.21036733964785}, - {"stationIdShort": "LUAS34", "stationId": "825GA00293", "shortCode": "SAN", "route": "Green", "name": "Sandyford", "lat": 53.2776301919892, "long": -6.20462123372981}, - {"stationIdShort": "LUAS37", "stationId": "825GA00310", "shortCode": "CPK", "route": "Green", "name": "Central Park", "lat": 53.2701680261693, "long": -6.20389941956604}, - {"stationIdShort": "LUAS38", "stationId": "825GA00313", "shortCode": "GLE", "route": "Green", "name": "Glencairn", "lat": 53.2663248994713, "long": -6.21013229223215}, - {"stationIdShort": "LUAS39", "stationId": "825GA00316", "shortCode": "GAL", "route": "Green", "name": "The Gallops", "lat": 53.2610932093991, "long": -6.20591369676934}, - {"stationIdShort": "LUAS40", "stationId": "825GA00319", "shortCode": "LEO", "route": "Green", "name": "Leopardstown Valley", "lat": 53.2582472581284, "long": -6.19838832357201}, - {"stationIdShort": "LUAS42", "stationId": "825GA00322", "shortCode": "BAW", "route": "Green", "name": "Ballyogan Wood", "lat": 53.2549979541227, "long": -6.18446604619674}, - {"stationIdShort": "LUAS44", "stationId": "825GA00325", "shortCode": "CCK", "route": "Green", "name": "Carrickmines", "lat": 53.2543609054348, "long": -6.17160341902405}, - {"stationIdShort": "LUAS46", "stationId": "825GA00329", "shortCode": "LAU", "route": "Green", "name": "Laughanstown", "lat": 53.2505997126709, "long": -6.15500876900899}, - {"stationIdShort": "LUAS47", "stationId": "825GA00332", "shortCode": "CHE", "route": "Green", "name": "Cherrywood", "lat": 53.2453444110287, "long": -6.14592931488556}, - {"stationIdShort": "LUAS48", "stationId": "825GA00335", "shortCode": "BRI", "route": "Green", "name": "Bride's Glen", "lat": 53.2418708352915, "long": -6.14278309230559, "type": "terminal"} -]} diff --git a/LuasKitIOS-Info.plist b/LuasKitIOS-Info.plist deleted file mode 100644 index b247395..0000000 --- a/LuasKitIOS-Info.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.12 - NSLocationAlwaysAndWhenInUseUsageDescription - Location access allows the app to find the closest LUAS stop - NSLocationAlwaysUsageDescription - Location access allows the app to find the closest LUAS stop - NSLocationUsageDescription - Location access allows the app to find the closest LUAS stop - CFBundleVersion - 104 - - diff --git a/LuasKitTests/APITests.swift b/LuasKitTests/APITests.swift deleted file mode 100644 index af99c0d..0000000 --- a/LuasKitTests/APITests.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// Created by Roland Gropmair on 19/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import XCTest - -@testable import LuasKit - -public struct WrongAPIWorker: APIWorker { - - public func getTrains(shortCode: String) async throws -> Data { - - let url = URL( - string: - "https://WRONGluasforecasts.rpa.ie/xml/get.ashx?action=forecast&stop=\(shortCode)&encrypt=false" - )! - - let (data, _) = try await URLSession.shared.data(from: url) - - return data - } - - public init() {} -} - -// this test class is shared between LuasKitIOSTests and the WatchKitExtensionTests - -class APITests: XCTestCase { - - func testRealAPI() async throws { - let api = LuasAPI(apiWorker: RealAPIWorker()) - - let trains = try await api.dueTimes(for: stationHarcourt) - - XCTAssertEqual(trains.trainStation.name, "Harcourt") - } - - func testWrongAPI() async { - let api = LuasAPI(apiWorker: WrongAPIWorker()) - - do { - _ = try await api.dueTimes(for: stationHarcourt) - XCTFail("unexpected") - } catch { - XCTAssertEqual( - error.localizedDescription, "A server with the specified hostname could not be found.") - } - } - - func testMockAPI_RanelaghTrains() async throws { - let api = LuasAPI(apiWorker: MockAPIWorker(scenario: .ranelaghTrains)) - - let trains = try await api.dueTimes(for: stationHarcourt) - - XCTAssertEqual(trains.inbound.count, 2) - XCTAssertEqual( - trains.inbound[0], Train(destination: "Broombridge", direction: "Inbound", dueTime: "Due")) - XCTAssertEqual( - trains.inbound[1], Train(destination: "Broombridge", direction: "Inbound", dueTime: "5")) - - XCTAssertEqual(trains.outbound.count, 3) - XCTAssertEqual( - trains.outbound[0], Train(destination: "Bride's Glen", direction: "Outbound", dueTime: "7")) - XCTAssertEqual( - trains.outbound[1], Train(destination: "Sandyford", direction: "Outbound", dueTime: "9")) - XCTAssertEqual( - trains.outbound[2], Train(destination: "Bride's Glen", direction: "Outbound", dueTime: "15")) - - XCTAssertEqual(trains.message, "Green Line services operating normally") - } - - func testMockAPI_NoTrains() async { - let api = LuasAPI(apiWorker: MockAPIWorker(scenario: .noTrainsButMessage)) - - do { - _ = try await api.dueTimes(for: stationHarcourt) - XCTFail("did expect an exception") - - } catch { - - if let apiError = error as? APIError { - - switch apiError { - case .noTrains(let message): - XCTAssertEqual(message, "Green Line services operating normally") - default: - XCTFail("unexpected case") - - } - } else { - XCTFail("unexpected case") - } - } - } - - func testMockAPI_NoTrainsNoMessage() async { - let api = LuasAPI(apiWorker: MockAPIWorker(scenario: .noTrainsNoMessage)) - - do { - _ = try await api.dueTimes(for: stationHarcourt) - XCTFail("did expect an exception") - - } catch { - - if let apiError = error as? APIError { - - switch apiError { - case .noTrains(let message): - XCTAssertEqual( - message, - "Couldn’t get any trains.\n\n" - + "Either Luas is not operating, or there is a problem with the Luas website.") - default: - XCTFail("unexpected case") - } - } else { - XCTFail("unexpected error type") - } - } - } - - func testMockAPI_ServerError() async { - let api = LuasAPI(apiWorker: MockAPIWorker(scenario: .serverError)) - - do { - _ = try await api.dueTimes(for: stationBluebell) - XCTFail("did expect an exception") - - } catch { - - XCTAssertEqual( - (error as NSError), NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)) - } - } - - func testMockAPI_ParserErrorInvalidXML() async { - let api = LuasAPI(apiWorker: MockAPIWorker(scenario: .parserError)) - - do { - _ = try await api.dueTimes(for: stationHarcourt) - XCTFail("did expect an exception") - - } catch { - - if let apiError = error as? APIError { - - switch apiError { - case .invalidXML(let message): - XCTAssertEqual(message, "some invalid xml") - default: - XCTFail("unexpected case") - } - } else { - XCTFail("unexpected error type") - } - } - } -} diff --git a/LuasKitTests/LuasKitTests.xctestplan b/LuasKitTests/LuasKitTests.xctestplan deleted file mode 100644 index e774486..0000000 --- a/LuasKitTests/LuasKitTests.xctestplan +++ /dev/null @@ -1,24 +0,0 @@ -{ - "configurations" : [ - { - "id" : "16648599-5577-435C-8B84-994ADF4BDEDA", - "name" : "Test Scheme Action", - "options" : { - - } - } - ], - "defaultOptions" : { - - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:LuasWatch.xcodeproj", - "identifier" : "AB0BD3462AE6F61700EB51AF", - "name" : "LuasKitTests" - } - } - ], - "version" : 1 -} diff --git a/LuasKitTests/ModelsTests.swift b/LuasKitTests/ModelsTests.swift deleted file mode 100644 index 69ce2aa..0000000 --- a/LuasKitTests/ModelsTests.swift +++ /dev/null @@ -1,228 +0,0 @@ -// -// Created by Roland Gropmair on 10/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import CoreLocation -import XCTest - -@testable import LuasKit - -// this test class is shared between LuasKitTests and the WatchKitExtensionTests - -class ModelsTests: XCTestCase { - - func testDueTimeDescription() { - - let train1 = Train(destination: "LUAS Broombridge", direction: "Outbound", dueTime: "DUE") - let train2 = Train(destination: "LUAS Broombridge", direction: "Outbound", dueTime: "9") - let train3 = Train(destination: "LUAS Sandyford", direction: "Inbound", dueTime: "12") - - let trains = TrainsByDirection( - trainStation: stationBluebell, - inbound: [train3], - outbound: [train1, train2]) - - XCTAssertEqual(trains.inbound.count, 1) - XCTAssertEqual(trains.inbound[0].dueTimeDescription, "Sandyford: 12 mins") - - XCTAssertEqual(trains.outbound.count, 2) - XCTAssertEqual(trains.outbound[0].dueTimeDescription, "Broombridge: Due") - XCTAssertEqual(trains.outbound[1].dueTimeDescription, "Broombridge: 9 mins") - } - - func testDestinationDueTimeDescription() { - let train1 = Train(destination: "LUAS Broombridge", direction: "Outbound", dueTime: "DUE") - let train2 = Train(destination: "LUAS Broombridge", direction: "Outbound", dueTime: "9") - let train3 = Train(destination: "LUAS Sandyford", direction: "Inbound", dueTime: "12") - - let trains = TrainsByDirection( - trainStation: stationBluebell, - inbound: [train3], - outbound: [train1, train2]) - - XCTAssertEqual(trains.inbound.count, 1) - XCTAssertEqual(trains.inbound[0].destinationDueTimeDescription, "Luas to LUAS Sandyford in 12") - - XCTAssertEqual(trains.outbound.count, 2) - XCTAssertEqual( - trains.outbound[0].destinationDueTimeDescription, "Luas to LUAS Broombridge is Due") - XCTAssertEqual( - trains.outbound[1].destinationDueTimeDescription, "Luas to LUAS Broombridge in 9") - } - - func testIsFinalStop() { - XCTAssertFalse(TrainStations.sharedFromFile.station(named: "Harcourt").isFinalStop) - XCTAssertTrue(TrainStations.sharedFromFile.station(named: "Broombridge").isFinalStop) - } - - func testGreenLineStations() { - XCTAssertEqual(TrainStations.sharedFromFile.greenLineStations.count, 35) - } - - func testRedLineStations() { - XCTAssertEqual(TrainStations.sharedFromFile.redLineStations.count, 32) - } - - func testClosestStation() { - let allStations = TrainStations(stations: [stationBluebell, stationHarcourt]) - - var location = CLLocation( - latitude: CLLocationDegrees(53.32928178728), longitude: CLLocationDegrees(-6.333825002759)) - XCTAssertEqual(allStations.closestStation(from: location)!.name, "Bluebell") - - location = CLLocation(latitude: CLLocationDegrees(53.329), longitude: CLLocationDegrees(-6.333)) - XCTAssertEqual(allStations.closestStation(from: location)!.name, "Bluebell") - - location = CLLocation(latitude: CLLocationDegrees(53.1), longitude: CLLocationDegrees(-6.333)) - XCTAssertNil(allStations.closestStation(from: location)) - - location = CLLocation(latitude: CLLocationDegrees(53.329), longitude: CLLocationDegrees(-6.333)) - XCTAssertEqual(allStations.closestStation(from: location, route: .red)!.name, "Bluebell") - XCTAssertEqual(allStations.closestStation(from: location, route: .green)!.name, "Harcourt") - } - - func testDistanceFromUserLocation() { - let locationNearHarcourt = - CLLocation( - latitude: stationHarcourt.location.coordinate.latitude + 0.001, - longitude: stationHarcourt.location.coordinate.longitude + 0.001) - - XCTAssertEqual(stationHarcourt.distance(from: locationNearHarcourt), nil) - - let locationFurtherAway = - CLLocation( - latitude: stationHarcourt.location.coordinate.latitude + 0.00425, - longitude: stationHarcourt.location.coordinate.longitude + 0.005) - - XCTAssertEqual(stationHarcourt.distance(from: locationFurtherAway), "600 m") - - let locationFarAway = - CLLocation( - latitude: stationHarcourt.location.coordinate.latitude + 0.0425, - longitude: stationHarcourt.location.coordinate.longitude + 0.05) - - XCTAssertEqual(stationHarcourt.distance(from: locationFarAway), "6 km") - } - - func testShortcutOutput() { - - let trains = TrainsByDirection( - trainStation: stationHarcourt, - inbound: [ - Train(destination: "Broombridge", direction: "Inbound", dueTime: "Due"), - Train(destination: "Broombridge", direction: "Inbound", dueTime: "12"), - ], - - outbound: [ - Train(destination: "Bride's Glen", direction: "Outbound", dueTime: "7"), - Train(destination: "Bride's Glen", direction: "Outbound", dueTime: "14"), - ], - message: "Phibsborough lift works until 28/04/23. See news.") - - let output = trains.shortcutOutput(direction: .both) - let expected = - """ - Luas to Broombridge is Due. - Luas to Broombridge in 12. - Luas to Bride's Glen in 7. - Luas to Bride's Glen in 14. - - """ - - XCTAssertEqual(expected, output) - } - - func testHasOverflow_noInbound_noOutbound_returns_InboundFalse_OutboundSmallAndLarge_false() { - - let trains = TrainsByDirection( - trainStation: stationHarcourt, - inbound: [], - outbound: []) - - XCTAssertEqual(trains.inboundHasOverflowSmall, false) - XCTAssertEqual(trains.outboundHasOverflowSmall, false) - XCTAssertEqual(trains.inboundNoOverflowSmall, []) - XCTAssertEqual(trains.outboundNoOverflowSmall, []) - - XCTAssertEqual(trains.inboundHasOverflowLarge, false) - XCTAssertEqual(trains.outboundHasOverflowLarge, false) - XCTAssertEqual(trains.inboundNoOverflowLarge, []) - XCTAssertEqual(trains.outboundNoOverflowLarge, []) - } - - func - testHasOverflow_threeInbound_threeOutbound_returns_InboundOutboundSmall_false_InbountLarge_false() - { - - let train = Train(destination: "Broombridge", direction: "Inbound", dueTime: "Due") - let trains = TrainsByDirection( - trainStation: stationHarcourt, - inbound: [train, train, train], - outbound: [train, train, train]) - - XCTAssertEqual(trains.inboundHasOverflowSmall, false) - XCTAssertEqual(trains.outboundHasOverflowSmall, false) - XCTAssertEqual(trains.inboundNoOverflowSmall, [train, train, train]) - XCTAssertEqual(trains.outboundNoOverflowSmall, [train, train, train]) - - XCTAssertEqual(trains.inboundHasOverflowLarge, false) - XCTAssertEqual(trains.outboundHasOverflowLarge, false) - XCTAssertEqual(trains.inboundNoOverflowLarge, [train, train, train]) - XCTAssertEqual(trains.outboundNoOverflowLarge, [train, train, train]) - } - - func - testHasOverflow_fourInbound_fourOutbound_returns_InboundOutboundSmall_true_InbountOutboundLarge_false() - { - - let train1 = Train(destination: "Broombridge1", direction: "Inbound", dueTime: "Due") - let train2 = Train(destination: "Broombridge2", direction: "Inbound", dueTime: "Due") - let train3 = Train(destination: "Broombridge3", direction: "Inbound", dueTime: "Due") - let train4 = Train(destination: "Broombridge4", direction: "Inbound", dueTime: "Due") - - let trains = TrainsByDirection( - trainStation: stationHarcourt, - inbound: [train1, train2, train3, train4], - outbound: [train1, train2, train3, train4]) - - XCTAssertEqual(trains.inboundHasOverflowSmall, true) - XCTAssertEqual(trains.outboundHasOverflowSmall, true) - XCTAssertEqual(trains.inboundNoOverflowSmall, [train1, train2, train3]) - XCTAssertEqual(trains.outboundNoOverflowSmall, [train1, train2, train3]) - - XCTAssertEqual(trains.inboundHasOverflowLarge, false) - XCTAssertEqual(trains.outboundHasOverflowLarge, false) - XCTAssertEqual(trains.inboundNoOverflowLarge, [train1, train2, train3, train4]) - XCTAssertEqual(trains.outboundNoOverflowLarge, [train1, train2, train3, train4]) - } - - func - testHasOverflow_sevenInbound_sevenOutbound_returns_InboundOutboundSmall_true_InbountOutboundLarge_true() - { - - let train1 = Train(destination: "Broombridge1", direction: "Inbound", dueTime: "Due") - let train2 = Train(destination: "Broombridge2", direction: "Inbound", dueTime: "Due") - let train3 = Train(destination: "Broombridge3", direction: "Inbound", dueTime: "Due") - let train4 = Train(destination: "Broombridge4", direction: "Inbound", dueTime: "Due") - let train5 = Train(destination: "Broombridge5", direction: "Inbound", dueTime: "Due") - let train6 = Train(destination: "Broombridge6", direction: "Inbound", dueTime: "Due") - let train7 = Train(destination: "Broombridge7", direction: "Inbound", dueTime: "Due") - - let trains = TrainsByDirection( - trainStation: stationHarcourt, - inbound: [train1, train2, train3, train4, train5, train6, train7], - outbound: [train1, train2, train3, train4, train5, train6, train7]) - - XCTAssertEqual(trains.inboundHasOverflowSmall, true) - XCTAssertEqual(trains.outboundHasOverflowSmall, true) - XCTAssertEqual(trains.inboundNoOverflowSmall, [train1, train2, train3]) - XCTAssertEqual(trains.outboundNoOverflowSmall, [train1, train2, train3]) - - XCTAssertEqual(trains.inboundHasOverflowLarge, true) - XCTAssertEqual(trains.outboundHasOverflowLarge, true) - XCTAssertEqual(trains.inboundNoOverflowLarge, [train1, train2, train3, train4, train5, train6]) - XCTAssertEqual( - trains.outboundNoOverflowLarge, [train1, train2, train3, train4, train5, train6]) - } -} diff --git a/LuasKitTests/ParserTests.swift b/LuasKitTests/ParserTests.swift deleted file mode 100644 index cf26a08..0000000 --- a/LuasKitTests/ParserTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Created by Roland Gropmair on 29/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import XCTest - -@testable import LuasKit - -class ParserTests: XCTestCase { - - func testMessageParsing() throws { - - // Apr 2023: looks like they fixed the XML now, escaping the apostrophe, so this fix is not that urgent anymore: - // No service Stephen\'s Green - Beechwood. See news - - let apiResponse = """ - - No service Stephen’s Green – Beechwood. See news - - - - - - - - """.data(using: .utf8)! - - let trainsByDirection = try APIParser.parse(xml: apiResponse, for: stationBluebell) - - XCTAssertEqual(trainsByDirection.inbound.count, 0) - XCTAssertEqual(trainsByDirection.outbound.count, 0) - XCTAssertEqual(trainsByDirection.message, "No service Stephen’s Green – Beechwood. See news") - } -} diff --git a/LuasKitTests/SnapshotTests.swift b/LuasKitTests/SnapshotTests.swift deleted file mode 100644 index 35b5384..0000000 --- a/LuasKitTests/SnapshotTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Created by Roland Gropmair on 10/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import XCTest - -@testable import LuasKit - -//import SnapshotTesting - -class SnapshotTests: XCTestCase { - - // func testSnapshot() { - // - // let shouldRecord = false - // - // let view = LuasView() - // .environmentObject(AppState(state: .errorGettingStation(LuasStrings.tooFarAway))) - // assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone8), - // traits: .init(userInterfaceStyle: .light)), - // named: "iPhone14Pro tooFarAway", record: shouldRecord) - // - // let viewTrains = LuasView() - // .environmentObject(AppState(state: .foundDueTimes(trainsRed_2_1, location))) - // assertSnapshot(matching: viewTrains, as: .image(layout: .device(config: .iPhone8), - // traits: .init(userInterfaceStyle: .light)), - // named: "iPhone14Pro trains", record: shouldRecord) - // - // // let viewError = LuasView() - // // .environmentObject( - // // AppState(state: .errorGettingDueTimes(String(format: LuasStrings.emptyDueTimesErrorMessage, "Cabra")))) - // // .environment(\.sizeCategory, .extraExtraLarge) - // // assertSnapshot(matching: viewError, as: .image(layout: .device(config: .iPhone8), - // // traits: .init(userInterfaceStyle: .light)), - // // named: "iPhone8 errorEmpty", record: shouldRecord) - // } -} diff --git a/LuasKitTests/TestDefinitions.swift b/LuasKitTests/TestDefinitions.swift deleted file mode 100644 index e12c85c..0000000 --- a/LuasKitTests/TestDefinitions.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Created by Roland Gropmair on 10/04/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import CoreLocation -import Foundation -import LuasKit - -extension TrainStations { - func station(named: String) -> TrainStation { - stations.filter({ $0.name == named }).first! - } -} - -let location = CLLocation(latitude: CLLocationDegrees(1.1), longitude: CLLocationDegrees(1.2)) - -let stationBluebell = TrainStation( - stationId: "822GA00360", - stationIdShort: "LUAS8", - shortCode: "BLU", - route: .red, - name: "Bluebell", - location: CLLocation( - latitude: CLLocationDegrees(53.3292817872831), - longitude: CLLocationDegrees(-6.33382500275916))) - -let stationHarcourt = TrainStation( - stationId: "822GA00440", - stationIdShort: "LUAS25", - shortCode: "HAR", - route: .green, - name: "Harcourt", - location: CLLocation( - latitude: CLLocationDegrees(53.3336246192981), - longitude: CLLocationDegrees(-6.26273785213714))) - -let trainRed1 = Train(destination: "LUAS The Point", direction: "Outbound", dueTime: "Due") -let trainRed2 = Train(destination: "LUAS Tallaght", direction: "Outbound", dueTime: "9") -let trainRed3 = Train(destination: "LUAS Connolly", direction: "Inbound", dueTime: "12") - -let stationRed = TrainStation( - stationId: "stationId", - stationIdShort: "LUAS8", - shortCode: "BLU", - route: .red, - name: "Bluebell", - location: location) - -let trainsRed_1_1 = TrainsByDirection( - trainStation: stationRed, - inbound: [trainRed3], - outbound: [trainRed2]) -let trainsRed_2_1 = TrainsByDirection( - trainStation: stationRed, - inbound: [trainRed1, trainRed3], - outbound: [trainRed2]) -let trainsRed_3_2 = TrainsByDirection( - trainStation: stationRed, - inbound: [trainRed1, trainRed2, trainRed3], - outbound: [trainRed1, trainRed2]) -let trainsRed_4_4 = TrainsByDirection( - trainStation: stationRed, - inbound: [trainRed1, trainRed2, trainRed3, trainRed3], - outbound: [trainRed1, trainRed2, trainRed3, trainRed3]) diff --git a/LuasPlayground.playground/Pages/new API .xcplaygroundpage/Contents.swift b/LuasPlayground.playground/Pages/new API .xcplaygroundpage/Contents.swift index 9146b7b..bd5a872 100644 --- a/LuasPlayground.playground/Pages/new API .xcplaygroundpage/Contents.swift +++ b/LuasPlayground.playground/Pages/new API .xcplaygroundpage/Contents.swift @@ -11,7 +11,8 @@ let station = TrainStation( shortCode: "BEE", route: .green, name: "Beechwood", - location: userLocation) + location: userLocation +) let realAPI = LuasAPI(session: URLSession.shared) diff --git a/LuasWatch Watch App/Coordinator+LocationDelegate.swift b/LuasWatch Watch App/Coordinator+LocationDelegate.swift index 2808300..76fc6fa 100644 --- a/LuasWatch Watch App/Coordinator+LocationDelegate.swift +++ b/LuasWatch Watch App/Coordinator+LocationDelegate.swift @@ -153,29 +153,42 @@ extension Coordinator: LocationDelegate { if let apiError = error as? APIError { switch apiError { - case .noTrains(let message): + + case .noTrainsButMessageFromAPI(let message): + updateWithAnimation( + to: .errorGettingDueTimes( + closestStation, + message)) + + case .noTrains: updateWithAnimation( - to: - .errorGettingDueTimes( - closestStation, - message.count > 0 - ? message : LuasStrings.errorGettingDueTimes(station: closestStation.name))) + to: .errorGettingDueTimes( + closestStation, + LuasStrings.noTrainsErrorMessage)) case .invalidXML: updateWithAnimation( - to: .errorGettingDueTimes(closestStation, "Error reading server response")) + to: .errorGettingDueTimes( + closestStation, + "Error reading server response")) } } else if (error as NSError).code == NSURLErrorNotConnectedToInternet { updateWithAnimation( to: .errorGettingDueTimes( closestStation, - LuasStrings.errorNoInternet)) + LuasStrings.errorNoInternet + ) + ) } else { updateWithAnimation( to: .errorGettingDueTimes( closestStation, - LuasStrings.errorGettingDueTimes(station: closestStation.name))) + LuasStrings.errorGettingDueTimes( + station: closestStation.name + ) + ) + ) } } } diff --git a/LuasWatch Watch App/Views/Sidebar/FavouritesSidebarView.swift b/LuasWatch Watch App/Views/Sidebar/FavouritesSidebarView.swift index a23ecd2..7d36602 100644 --- a/LuasWatch Watch App/Views/Sidebar/FavouritesSidebarView.swift +++ b/LuasWatch Watch App/Views/Sidebar/FavouritesSidebarView.swift @@ -29,7 +29,7 @@ extension FavouritesSidebarView: View { let station = Self.trainStations.station(shortCode: station.shortCode) - ?? TrainStations.unknown + ?? TrainStation.unknown StationRow( station: station, diff --git a/LuasWatch.xcodeproj/project.pbxproj b/LuasWatch.xcodeproj/project.pbxproj index 59e53de..0d22078 100644 --- a/LuasWatch.xcodeproj/project.pbxproj +++ b/LuasWatch.xcodeproj/project.pbxproj @@ -9,29 +9,13 @@ /* Begin PBXBuildFile section */ AB0A86CC2B460C74002E7E31 /* AllStationsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0A86CB2B460C74002E7E31 /* AllStationsListView.swift */; }; AB0A86CE2B461767002E7E31 /* Collection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0A86CD2B461767002E7E31 /* Collection+Extensions.swift */; }; - AB0BD34B2AE6F61700EB51AF /* LuasKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB57F0DD230B3C1900BA8B53 /* LuasKit.framework */; }; - AB0BD3562AE6F64C00EB51AF /* ModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3512AE6F64C00EB51AF /* ModelsTests.swift */; }; - AB0BD3572AE6F64C00EB51AF /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3522AE6F64C00EB51AF /* ParserTests.swift */; }; - AB0BD3582AE6F64C00EB51AF /* APITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3532AE6F64C00EB51AF /* APITests.swift */; }; - AB0BD3592AE6F64C00EB51AF /* SnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3542AE6F64C00EB51AF /* SnapshotTests.swift */; }; - AB0BD35A2AE6F64C00EB51AF /* TestDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3552AE6F64C00EB51AF /* TestDefinitions.swift */; }; AB0BD3632AE7062300EB51AF /* LuasWatchiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3622AE7062300EB51AF /* LuasWatchiOSApp.swift */; }; AB0BD3672AE7062400EB51AF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB0BD3662AE7062400EB51AF /* Assets.xcassets */; }; AB0BD36A2AE7062400EB51AF /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB0BD3692AE7062400EB51AF /* Preview Assets.xcassets */; }; AB0BD3742AE7062400EB51AF /* LuasWatchiOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3732AE7062400EB51AF /* LuasWatchiOSTests.swift */; }; AB0BD37E2AE7062400EB51AF /* LuasWatchiOSUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD37D2AE7062400EB51AF /* LuasWatchiOSUITests.swift */; }; AB0BD3802AE7062400EB51AF /* LuasWatchiOSUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD37F2AE7062400EB51AF /* LuasWatchiOSUITestsLaunchTests.swift */; }; - AB0BD39A2AE7071200EB51AF /* GrantLocationAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD38B2AE7071200EB51AF /* GrantLocationAuthView.swift */; }; - AB0BD39B2AE7071200EB51AF /* LoadingAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD38C2AE7071200EB51AF /* LoadingAnimationView.swift */; }; - AB0BD39C2AE7071200EB51AF /* TrainsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD38D2AE7071200EB51AF /* TrainsListView.swift */; }; - AB0BD39D2AE7071200EB51AF /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD38E2AE7071200EB51AF /* HeaderView.swift */; }; - AB0BD39E2AE7071200EB51AF /* ChangeStationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD38F2AE7071200EB51AF /* ChangeStationButton.swift */; }; - AB0BD39F2AE7071200EB51AF /* TapOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3902AE7071200EB51AF /* TapOverlayView.swift */; }; - AB0BD3A02AE7071200EB51AF /* LuasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3912AE7071200EB51AF /* LuasView.swift */; }; - AB0BD3A22AE7071200EB51AF /* PreviewsDataiOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3942AE7071200EB51AF /* PreviewsDataiOS.swift */; }; AB0BD3A92AE7086700EB51AF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BD3A82AE7086700EB51AF /* Coordinator.swift */; }; - AB0BD3AB2AE70AFD00EB51AF /* LuasKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB57F0DD230B3C1900BA8B53 /* LuasKit.framework */; }; - AB0BD3AC2AE70AFD00EB51AF /* LuasKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AB57F0DD230B3C1900BA8B53 /* LuasKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; AB0BFCE92B42E5430060AAD5 /* StationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BFCE82B42E5430060AAD5 /* StationView.swift */; }; AB0BFCEB2B42E58A0060AAD5 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BFCEA2B42E58A0060AAD5 /* SidebarView.swift */; }; AB0BFCED2B42E7780060AAD5 /* FavouritesSidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BFCEC2B42E7780060AAD5 /* FavouritesSidebarView.swift */; }; @@ -45,25 +29,48 @@ AB2453F82AE424B40007FA7C /* LuasWatchComplications.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB2453F72AE424B40007FA7C /* LuasWatchComplications.swift */; }; AB2453FA2AE424B50007FA7C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB2453F92AE424B50007FA7C /* Assets.xcassets */; }; AB2453FE2AE424B50007FA7C /* LuasWatchComplicationsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AB2453F12AE424B40007FA7C /* LuasWatchComplicationsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + AB3095A62D2315E100955598 /* LuasAPI in Frameworks */ = {isa = PBXBuildFile; productRef = AB3095A52D2315E100955598 /* LuasAPI */; }; + AB3095A82D2315E100955598 /* LuasApp in Frameworks */ = {isa = PBXBuildFile; productRef = AB3095A72D2315E100955598 /* LuasApp */; }; + AB3095AB2D23173200955598 /* Coordinator+LocationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095AA2D23173200955598 /* Coordinator+LocationDelegate.swift */; }; + AB3095AC2D23173200955598 /* Coordinator+Animiation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095A92D23173200955598 /* Coordinator+Animiation.swift */; }; + AB3095AE2D23177300955598 /* PreviewsData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095AD2D23177300955598 /* PreviewsData.swift */; }; + AB3095B02D23179C00955598 /* CLLocation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095AF2D23179C00955598 /* CLLocation+Extensions.swift */; }; + AB3095B22D2317F200955598 /* LuasMainScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B12D2317F200955598 /* LuasMainScreen.swift */; }; + AB3095BD2D23182700955598 /* AllStationsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B32D23182700955598 /* AllStationsListView.swift */; }; + AB3095BE2D23182700955598 /* FavouritesSidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B52D23182700955598 /* FavouritesSidebarView.swift */; }; + AB3095BF2D23182700955598 /* StationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095BA2D23182700955598 /* StationRow.swift */; }; + AB3095C02D23182700955598 /* NearbyStationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B82D23182700955598 /* NearbyStationsView.swift */; }; + AB3095C12D23182700955598 /* StationsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095BB2D23182700955598 /* StationsModal.swift */; }; + AB3095C22D23182700955598 /* LineRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B62D23182700955598 /* LineRow.swift */; }; + AB3095C32D23182700955598 /* LinesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B72D23182700955598 /* LinesView.swift */; }; + AB3095C42D23182700955598 /* FavouritesHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B42D23182700955598 /* FavouritesHeaderView.swift */; }; + AB3095C52D23182700955598 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095B92D23182700955598 /* SidebarView.swift */; }; + AB3095D62D23186C00955598 /* LuasTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095CA2D23186C00955598 /* LuasTextView.swift */; }; + AB3095D72D23186C00955598 /* TrainsViewLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095D32D23186C00955598 /* TrainsViewLoading.swift */; }; + AB3095D82D23186C00955598 /* SimpleTimetableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095CD2D23186C00955598 /* SimpleTimetableView.swift */; }; + AB3095D92D23186C00955598 /* StationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095D12D23186C00955598 /* StationView.swift */; }; + AB3095DA2D23186C00955598 /* StationToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095CF2D23186C00955598 /* StationToolbar.swift */; }; + AB3095DB2D23186C00955598 /* OverflowDotsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095CC2D23186C00955598 /* OverflowDotsView.swift */; }; + AB3095DC2D23186C00955598 /* GrantLocationAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095C92D23186C00955598 /* GrantLocationAuthView.swift */; }; + AB3095DD2D23186C00955598 /* StationTimesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095CE2D23186C00955598 /* StationTimesView.swift */; }; + AB3095DE2D23186C00955598 /* View+LuasFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095D42D23186C00955598 /* View+LuasFormatting.swift */; }; + AB3095DF2D23186C00955598 /* Collection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095C62D23186C00955598 /* Collection+Extensions.swift */; }; + AB3095E02D23186C00955598 /* StationToolbarLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095D02D23186C00955598 /* StationToolbarLoading.swift */; }; + AB3095E12D23186C00955598 /* ToolbarInactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095D22D23186C00955598 /* ToolbarInactive.swift */; }; + AB3095E22D23186C00955598 /* DoubleTimetableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095C72D23186C00955598 /* DoubleTimetableView.swift */; }; + AB3095E32D23186C00955598 /* NoTrainsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095CB2D23186C00955598 /* NoTrainsView.swift */; }; + AB3095E42D23186C00955598 /* DueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095C82D23186C00955598 /* DueView.swift */; }; + AB3095EC2D2318A400955598 /* PreviewsHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095E62D2318A400955598 /* PreviewsHelpers.swift */; }; + AB3095ED2D2318A400955598 /* PreviewsSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095E82D2318A400955598 /* PreviewsSidebar.swift */; }; + AB3095EF2D2318A400955598 /* PreviewsStationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095EA2D2318A400955598 /* PreviewsStationView.swift */; }; + AB3095F02D2318A400955598 /* Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095E52D2318A400955598 /* Previews.swift */; }; + AB3095F12D2318A400955598 /* PreviewsLoadingStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095E72D2318A400955598 /* PreviewsLoadingStates.swift */; }; + AB3095F22D2318A400955598 /* PreviewsStationView_EdgeCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095EB2D2318A400955598 /* PreviewsStationView_EdgeCases.swift */; }; + AB3095F72D2318E200955598 /* ModelContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095F42D2318E200955598 /* ModelContext+Extensions.swift */; }; + AB3095F82D2318E200955598 /* FavouriteStation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095F32D2318E200955598 /* FavouriteStation.swift */; }; + AB3095F92D2318E200955598 /* StationDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3095F52D2318E200955598 /* StationDirection.swift */; }; AB38CECE2B52F16C0074D901 /* Coordinator+LocationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB38CECD2B52F16C0074D901 /* Coordinator+LocationDelegate.swift */; }; AB38CED02B52F28C0074D901 /* CLLocation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB38CECF2B52F28C0074D901 /* CLLocation+Extensions.swift */; }; - AB3CC01B2AE4094F002AA60A /* APIParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0082AE4094F002AA60A /* APIParser.swift */; }; - AB3CC01C2AE4094F002AA60A /* LuasMockAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0092AE4094F002AA60A /* LuasMockAPI.swift */; }; - AB3CC01D2AE4094F002AA60A /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC00A2AE4094F002AA60A /* API.swift */; }; - AB3CC01E2AE4094F002AA60A /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC00B2AE4094F002AA60A /* Parser.swift */; }; - AB3CC0202AE4094F002AA60A /* luasStops.json in Resources */ = {isa = PBXBuildFile; fileRef = AB3CC00D2AE4094F002AA60A /* luasStops.json */; }; - AB3CC0212AE4094F002AA60A /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC00E2AE4094F002AA60A /* Location.swift */; }; - AB3CC0222AE4094F002AA60A /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC00F2AE4094F002AA60A /* Logging.swift */; }; - AB3CC0232AE4094F002AA60A /* LuasStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0102AE4094F002AA60A /* LuasStrings.swift */; }; - AB3CC0242AE4094F002AA60A /* TrainsByDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0122AE4094F002AA60A /* TrainsByDirection.swift */; }; - AB3CC0252AE4094F002AA60A /* TrainStation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0132AE4094F002AA60A /* TrainStation.swift */; }; - AB3CC0262AE4094F002AA60A /* Train.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0142AE4094F002AA60A /* Train.swift */; }; - AB3CC0272AE4094F002AA60A /* TrainStations.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0152AE4094F002AA60A /* TrainStations.swift */; }; - AB3CC0282AE4094F002AA60A /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0162AE4094F002AA60A /* Route.swift */; }; - AB3CC0292AE4094F002AA60A /* Direction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0172AE4094F002AA60A /* Direction.swift */; }; - AB3CC02A2AE4094F002AA60A /* MyUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0182AE4094F002AA60A /* MyUserDefaults.swift */; }; - AB3CC02B2AE4094F002AA60A /* LuasExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3CC0192AE4094F002AA60A /* LuasExtensions.swift */; }; - AB3CC02C2AE4094F002AA60A /* LuasKit.h in Headers */ = {isa = PBXBuildFile; fileRef = AB3CC01A2AE4094F002AA60A /* LuasKit.h */; }; AB69277A2CE0D87C0076F9E3 /* PreviewsSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6927792CE0D87C0076F9E3 /* PreviewsSidebar.swift */; }; AB695E2E2AE41C1D00B6DB99 /* LuasWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = AB695E2D2AE41C1C00B6DB99 /* LuasWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AB695E332AE41C1D00B6DB99 /* LuasWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB695E322AE41C1D00B6DB99 /* LuasWatchApp.swift */; }; @@ -77,22 +84,18 @@ AB695E842AE41EAE00B6DB99 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB695E6F2AE41CAC00B6DB99 /* Preview Assets.xcassets */; }; AB79B57D2D21A614001E20F8 /* LuasAPI in Frameworks */ = {isa = PBXBuildFile; productRef = AB79B57C2D21A614001E20F8 /* LuasAPI */; }; AB79B57F2D21A614001E20F8 /* LuasApp in Frameworks */ = {isa = PBXBuildFile; productRef = AB79B57E2D21A614001E20F8 /* LuasApp */; }; - AB8220792B49F79200022CA0 /* AppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8220782B49F79200022CA0 /* AppModel.swift */; }; AB89FB892B49B6BE0020A943 /* StationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB89FB882B49B6BE0020A943 /* StationRow.swift */; }; AB89FB8B2B49B7B50020A943 /* LineRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB89FB8A2B49B7B50020A943 /* LineRow.swift */; }; AB925F472B6FB91500F23497 /* ModelContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB925F462B6FB91500F23497 /* ModelContext+Extensions.swift */; }; AB925F492B6FCD0800F23497 /* StationDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB925F482B6FCD0800F23497 /* StationDirection.swift */; }; - AB925F4B2B715FAD00F23497 /* AppModel+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB925F4A2B715FAD00F23497 /* AppModel+Codable.swift */; }; - AB925F4D2B715FCD00F23497 /* AppModel+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB925F4C2B715FCD00F23497 /* AppModel+CustomStringConvertible.swift */; }; + AB96C8AF2D4EA972007B493C /* PreviewsStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB96C8AE2D4EA972007B493C /* PreviewsStates.swift */; }; + AB96C8B12D502915007B493C /* ClosestStationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB96C8B02D502915007B493C /* ClosestStationsView.swift */; }; AB9D3F092BF409EA00584B69 /* PreviewsLoadingStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB9D3F082BF409EA00584B69 /* PreviewsLoadingStates.swift */; }; ABA68B242AF43977002A9F0D /* LuasMainScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA68B232AF43977002A9F0D /* LuasMainScreen.swift */; }; ABC207AB2B78E34A00C1F8F7 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC207AA2B78E34A00C1F8F7 /* Utils.swift */; }; - ABC207AD2B8156BB00C1F8F7 /* CLAuthorizationStatus+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC207AC2B8156BB00C1F8F7 /* CLAuthorizationStatus+Extensions.swift */; }; ABC207AF2B81653200C1F8F7 /* Coordinator+Animiation.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC207AE2B81653200C1F8F7 /* Coordinator+Animiation.swift */; }; ABC24A7B2C0289D30004014B /* StationToolbarLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC24A7A2C0289D30004014B /* StationToolbarLoading.swift */; }; ABC24A7D2C028E120004014B /* TrainsViewLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC24A7C2C028E120004014B /* TrainsViewLoading.swift */; }; - ABC24A7F2C0525F50004014B /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC24A7E2C0525F50004014B /* AppState.swift */; }; - ABC24A812C0526950004014B /* AppMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC24A802C0526950004014B /* AppMode.swift */; }; ABC24A832C052DBB0004014B /* ToolbarInactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC24A822C052DBB0004014B /* ToolbarInactive.swift */; }; ABCDB4C42B5DADE40068EFAA /* PreviewsStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCDB4C32B5DADE40068EFAA /* PreviewsStates.swift */; }; ABD1CA4E2B781DDD0015DB81 /* DueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD1CA4D2B781DDD0015DB81 /* DueView.swift */; }; @@ -111,13 +114,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - AB0BD34C2AE6F61700EB51AF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = AB364CF92308276C00C73EF6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = AB57F0DC230B3C1900BA8B53; - remoteInfo = LuasKit; - }; AB0BD3702AE7062400EB51AF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AB364CF92308276C00C73EF6 /* Project object */; @@ -163,17 +159,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - AB0BD3AD2AE70AFD00EB51AF /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - AB0BD3AC2AE70AFD00EB51AF /* LuasKit.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; AB2453FF2AE424B50007FA7C /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -211,13 +196,6 @@ /* Begin PBXFileReference section */ AB0A86CB2B460C74002E7E31 /* AllStationsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllStationsListView.swift; sourceTree = ""; }; AB0A86CD2B461767002E7E31 /* Collection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Extensions.swift"; sourceTree = ""; }; - AB0BD3472AE6F61700EB51AF /* LuasKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LuasKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - AB0BD3512AE6F64C00EB51AF /* ModelsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelsTests.swift; sourceTree = ""; }; - AB0BD3522AE6F64C00EB51AF /* ParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParserTests.swift; sourceTree = ""; }; - AB0BD3532AE6F64C00EB51AF /* APITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APITests.swift; sourceTree = ""; }; - AB0BD3542AE6F64C00EB51AF /* SnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotTests.swift; sourceTree = ""; }; - AB0BD3552AE6F64C00EB51AF /* TestDefinitions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDefinitions.swift; sourceTree = ""; }; - AB0BD35B2AE6F77300EB51AF /* LuasKitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = LuasKitTests.xctestplan; sourceTree = ""; }; AB0BD3602AE7062200EB51AF /* LuasWatchiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LuasWatchiOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; AB0BD3622AE7062300EB51AF /* LuasWatchiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuasWatchiOSApp.swift; sourceTree = ""; }; AB0BD3662AE7062400EB51AF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -227,19 +205,6 @@ AB0BD3792AE7062400EB51AF /* LuasWatchiOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LuasWatchiOSUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AB0BD37D2AE7062400EB51AF /* LuasWatchiOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuasWatchiOSUITests.swift; sourceTree = ""; }; AB0BD37F2AE7062400EB51AF /* LuasWatchiOSUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuasWatchiOSUITestsLaunchTests.swift; sourceTree = ""; }; - AB0BD38B2AE7071200EB51AF /* GrantLocationAuthView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GrantLocationAuthView.swift; sourceTree = ""; }; - AB0BD38C2AE7071200EB51AF /* LoadingAnimationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingAnimationView.swift; sourceTree = ""; }; - AB0BD38D2AE7071200EB51AF /* TrainsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainsListView.swift; sourceTree = ""; }; - AB0BD38E2AE7071200EB51AF /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; - AB0BD38F2AE7071200EB51AF /* ChangeStationButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeStationButton.swift; sourceTree = ""; }; - AB0BD3902AE7071200EB51AF /* TapOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapOverlayView.swift; sourceTree = ""; }; - AB0BD3912AE7071200EB51AF /* LuasView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LuasView.swift; sourceTree = ""; }; - AB0BD3932AE7071200EB51AF /* PreviewsStationsModal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewsStationsModal.swift; sourceTree = ""; }; - AB0BD3942AE7071200EB51AF /* PreviewsDataiOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewsDataiOS.swift; sourceTree = ""; }; - AB0BD3952AE7071200EB51AF /* PreviewsTapOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewsTapOverlay.swift; sourceTree = ""; }; - AB0BD3962AE7071200EB51AF /* PreviewsAppStartup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewsAppStartup.swift; sourceTree = ""; }; - AB0BD3972AE7071200EB51AF /* PreviewsAppResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewsAppResult.swift; sourceTree = ""; }; - AB0BD3982AE7071200EB51AF /* PreviewsAppRunning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewsAppRunning.swift; sourceTree = ""; }; AB0BD3A82AE7086700EB51AF /* Coordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; AB0BFCE82B42E5430060AAD5 /* StationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationView.swift; sourceTree = ""; }; AB0BFCEA2B42E58A0060AAD5 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; @@ -256,29 +221,48 @@ AB2453F92AE424B50007FA7C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AB2453FB2AE424B50007FA7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AB3095A42D23039A00955598 /* LuasAppTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = LuasAppTestPlan.xctestplan; path = LuasApp/Tests/LuasAppTestPlan.xctestplan; sourceTree = ""; }; + AB3095A92D23173200955598 /* Coordinator+Animiation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Animiation.swift"; sourceTree = ""; }; + AB3095AA2D23173200955598 /* Coordinator+LocationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+LocationDelegate.swift"; sourceTree = ""; }; + AB3095AD2D23177300955598 /* PreviewsData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsData.swift; sourceTree = ""; }; + AB3095AF2D23179C00955598 /* CLLocation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLLocation+Extensions.swift"; sourceTree = ""; }; + AB3095B12D2317F200955598 /* LuasMainScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuasMainScreen.swift; sourceTree = ""; }; + AB3095B32D23182700955598 /* AllStationsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllStationsListView.swift; sourceTree = ""; }; + AB3095B42D23182700955598 /* FavouritesHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouritesHeaderView.swift; sourceTree = ""; }; + AB3095B52D23182700955598 /* FavouritesSidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouritesSidebarView.swift; sourceTree = ""; }; + AB3095B62D23182700955598 /* LineRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineRow.swift; sourceTree = ""; }; + AB3095B72D23182700955598 /* LinesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinesView.swift; sourceTree = ""; }; + AB3095B82D23182700955598 /* NearbyStationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyStationsView.swift; sourceTree = ""; }; + AB3095B92D23182700955598 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; + AB3095BA2D23182700955598 /* StationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationRow.swift; sourceTree = ""; }; + AB3095BB2D23182700955598 /* StationsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationsModal.swift; sourceTree = ""; }; + AB3095C62D23186C00955598 /* Collection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Extensions.swift"; sourceTree = ""; }; + AB3095C72D23186C00955598 /* DoubleTimetableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleTimetableView.swift; sourceTree = ""; }; + AB3095C82D23186C00955598 /* DueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DueView.swift; sourceTree = ""; }; + AB3095C92D23186C00955598 /* GrantLocationAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrantLocationAuthView.swift; sourceTree = ""; }; + AB3095CA2D23186C00955598 /* LuasTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuasTextView.swift; sourceTree = ""; }; + AB3095CB2D23186C00955598 /* NoTrainsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoTrainsView.swift; sourceTree = ""; }; + AB3095CC2D23186C00955598 /* OverflowDotsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverflowDotsView.swift; sourceTree = ""; }; + AB3095CD2D23186C00955598 /* SimpleTimetableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTimetableView.swift; sourceTree = ""; }; + AB3095CE2D23186C00955598 /* StationTimesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationTimesView.swift; sourceTree = ""; }; + AB3095CF2D23186C00955598 /* StationToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationToolbar.swift; sourceTree = ""; }; + AB3095D02D23186C00955598 /* StationToolbarLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationToolbarLoading.swift; sourceTree = ""; }; + AB3095D12D23186C00955598 /* StationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationView.swift; sourceTree = ""; }; + AB3095D22D23186C00955598 /* ToolbarInactive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarInactive.swift; sourceTree = ""; }; + AB3095D32D23186C00955598 /* TrainsViewLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainsViewLoading.swift; sourceTree = ""; }; + AB3095D42D23186C00955598 /* View+LuasFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+LuasFormatting.swift"; sourceTree = ""; }; + AB3095E52D2318A400955598 /* Previews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previews.swift; sourceTree = ""; }; + AB3095E62D2318A400955598 /* PreviewsHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsHelpers.swift; sourceTree = ""; }; + AB3095E72D2318A400955598 /* PreviewsLoadingStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsLoadingStates.swift; sourceTree = ""; }; + AB3095E82D2318A400955598 /* PreviewsSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsSidebar.swift; sourceTree = ""; }; + AB3095EA2D2318A400955598 /* PreviewsStationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsStationView.swift; sourceTree = ""; }; + AB3095EB2D2318A400955598 /* PreviewsStationView_EdgeCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsStationView_EdgeCases.swift; sourceTree = ""; }; + AB3095F32D2318E200955598 /* FavouriteStation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteStation.swift; sourceTree = ""; }; + AB3095F42D2318E200955598 /* ModelContext+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelContext+Extensions.swift"; sourceTree = ""; }; + AB3095F52D2318E200955598 /* StationDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationDirection.swift; sourceTree = ""; }; AB387B402CBBE202002D4B4A /* myConfiguration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = myConfiguration.json; sourceTree = ""; }; AB38CECD2B52F16C0074D901 /* Coordinator+LocationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+LocationDelegate.swift"; sourceTree = ""; }; AB38CECF2B52F28C0074D901 /* CLLocation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLLocation+Extensions.swift"; sourceTree = ""; }; - AB3CC0082AE4094F002AA60A /* APIParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIParser.swift; sourceTree = ""; }; - AB3CC0092AE4094F002AA60A /* LuasMockAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LuasMockAPI.swift; sourceTree = ""; }; - AB3CC00A2AE4094F002AA60A /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; - AB3CC00B2AE4094F002AA60A /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; - AB3CC00D2AE4094F002AA60A /* luasStops.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = luasStops.json; sourceTree = ""; }; - AB3CC00E2AE4094F002AA60A /* Location.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; - AB3CC00F2AE4094F002AA60A /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; - AB3CC0102AE4094F002AA60A /* LuasStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LuasStrings.swift; sourceTree = ""; }; - AB3CC0122AE4094F002AA60A /* TrainsByDirection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainsByDirection.swift; sourceTree = ""; }; - AB3CC0132AE4094F002AA60A /* TrainStation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainStation.swift; sourceTree = ""; }; - AB3CC0142AE4094F002AA60A /* Train.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Train.swift; sourceTree = ""; }; - AB3CC0152AE4094F002AA60A /* TrainStations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainStations.swift; sourceTree = ""; }; - AB3CC0162AE4094F002AA60A /* Route.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; - AB3CC0172AE4094F002AA60A /* Direction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Direction.swift; sourceTree = ""; }; - AB3CC0182AE4094F002AA60A /* MyUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyUserDefaults.swift; sourceTree = ""; }; - AB3CC0192AE4094F002AA60A /* LuasExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LuasExtensions.swift; sourceTree = ""; }; - AB3CC01A2AE4094F002AA60A /* LuasKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LuasKit.h; sourceTree = ""; }; - AB57F0DD230B3C1900BA8B53 /* LuasKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LuasKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - AB57F0E0230B3C1900BA8B53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AB57F129230B432F00BA8B53 /* LuasKitIOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "LuasKitIOS-Info.plist"; path = "/Users/rolandg/src/LuasWatch/LuasKitIOS-Info.plist"; sourceTree = ""; }; + AB50A2082D4C26F0002A2DB8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; AB6927792CE0D87C0076F9E3 /* PreviewsSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsSidebar.swift; sourceTree = ""; }; AB695E282AE41C1C00B6DB99 /* LuasWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LuasWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; AB695E2D2AE41C1C00B6DB99 /* LuasWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "LuasWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -296,24 +280,20 @@ AB79B57A2D219ADE001E20F8 /* LuasAPI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LuasAPI; sourceTree = ""; }; AB79B57B2D219B9E001E20F8 /* LuasApp */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LuasApp; sourceTree = ""; }; AB79B5802D230098001E20F8 /* LuasAPITestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = LuasAPITestPlan.xctestplan; sourceTree = ""; }; - AB8220782B49F79200022CA0 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = ""; }; AB89FB882B49B6BE0020A943 /* StationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationRow.swift; sourceTree = ""; }; AB89FB8A2B49B7B50020A943 /* LineRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineRow.swift; sourceTree = ""; }; AB925F442B6FAE5700F23497 /* LuasWatch Watch App.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "LuasWatch Watch App.xctestplan"; sourceTree = ""; }; AB925F462B6FB91500F23497 /* ModelContext+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelContext+Extensions.swift"; sourceTree = ""; }; AB925F482B6FCD0800F23497 /* StationDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationDirection.swift; sourceTree = ""; }; - AB925F4A2B715FAD00F23497 /* AppModel+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppModel+Codable.swift"; sourceTree = ""; }; - AB925F4C2B715FCD00F23497 /* AppModel+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppModel+CustomStringConvertible.swift"; sourceTree = ""; }; + AB96C8AE2D4EA972007B493C /* PreviewsStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsStates.swift; sourceTree = ""; }; + AB96C8B02D502915007B493C /* ClosestStationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosestStationsView.swift; sourceTree = ""; }; AB9D3F082BF409EA00584B69 /* PreviewsLoadingStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsLoadingStates.swift; sourceTree = ""; }; ABA68B232AF43977002A9F0D /* LuasMainScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuasMainScreen.swift; sourceTree = ""; }; ABAFF5B424ED58A8004DB119 /* LuasPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = LuasPlayground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; ABC207AA2B78E34A00C1F8F7 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - ABC207AC2B8156BB00C1F8F7 /* CLAuthorizationStatus+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLAuthorizationStatus+Extensions.swift"; sourceTree = ""; }; ABC207AE2B81653200C1F8F7 /* Coordinator+Animiation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Coordinator+Animiation.swift"; sourceTree = ""; }; ABC24A7A2C0289D30004014B /* StationToolbarLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StationToolbarLoading.swift; sourceTree = ""; }; ABC24A7C2C028E120004014B /* TrainsViewLoading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainsViewLoading.swift; sourceTree = ""; }; - ABC24A7E2C0525F50004014B /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; - ABC24A802C0526950004014B /* AppMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMode.swift; sourceTree = ""; }; ABC24A822C052DBB0004014B /* ToolbarInactive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarInactive.swift; sourceTree = ""; }; ABCDB4C32B5DADE40068EFAA /* PreviewsStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsStates.swift; sourceTree = ""; }; ABD1CA4D2B781DDD0015DB81 /* DueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DueView.swift; sourceTree = ""; }; @@ -332,19 +312,12 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - AB0BD3442AE6F61700EB51AF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - AB0BD34B2AE6F61700EB51AF /* LuasKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; AB0BD35D2AE7062200EB51AF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AB0BD3AB2AE70AFD00EB51AF /* LuasKit.framework in Frameworks */, + AB3095A62D2315E100955598 /* LuasAPI in Frameworks */, + AB3095A82D2315E100955598 /* LuasApp in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -371,13 +344,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - AB57F0DA230B3C1900BA8B53 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; AB695E2A2AE41C1C00B6DB99 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -404,26 +370,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - AB0BD3482AE6F61700EB51AF /* LuasKitTests */ = { - isa = PBXGroup; - children = ( - AB0BD35B2AE6F77300EB51AF /* LuasKitTests.xctestplan */, - AB0BD3532AE6F64C00EB51AF /* APITests.swift */, - AB0BD3512AE6F64C00EB51AF /* ModelsTests.swift */, - AB0BD3522AE6F64C00EB51AF /* ParserTests.swift */, - AB0BD3542AE6F64C00EB51AF /* SnapshotTests.swift */, - AB0BD3552AE6F64C00EB51AF /* TestDefinitions.swift */, - ); - path = LuasKitTests; - sourceTree = ""; - }; AB0BD3612AE7062300EB51AF /* LuasWatchiOS */ = { isa = PBXGroup; children = ( + AB0BD3622AE7062300EB51AF /* LuasWatchiOSApp.swift */, + AB3095AF2D23179C00955598 /* CLLocation+Extensions.swift */, AB0BD3A82AE7086700EB51AF /* Coordinator.swift */, - AB0BD3922AE7071200EB51AF /* Previews */, + AB3095A92D23173200955598 /* Coordinator+Animiation.swift */, + AB3095AA2D23173200955598 /* Coordinator+LocationDelegate.swift */, + AB3095F62D2318E200955598 /* Models */, AB0BD38A2AE7071200EB51AF /* Views */, - AB0BD3622AE7062300EB51AF /* LuasWatchiOSApp.swift */, + AB0BD3922AE7071200EB51AF /* Previews */, AB0BD3662AE7062400EB51AF /* Assets.xcassets */, AB0BD3682AE7062400EB51AF /* Preview Content */, ); @@ -458,13 +415,10 @@ AB0BD38A2AE7071200EB51AF /* Views */ = { isa = PBXGroup; children = ( - AB0BD38B2AE7071200EB51AF /* GrantLocationAuthView.swift */, - AB0BD38C2AE7071200EB51AF /* LoadingAnimationView.swift */, - AB0BD38D2AE7071200EB51AF /* TrainsListView.swift */, - AB0BD38E2AE7071200EB51AF /* HeaderView.swift */, - AB0BD38F2AE7071200EB51AF /* ChangeStationButton.swift */, - AB0BD3902AE7071200EB51AF /* TapOverlayView.swift */, - AB0BD3912AE7071200EB51AF /* LuasView.swift */, + AB3095B12D2317F200955598 /* LuasMainScreen.swift */, + AB3095D52D23186C00955598 /* DetailView */, + AB3095BC2D23182700955598 /* Sidebar */, + AB3095C62D23186C00955598 /* Collection+Extensions.swift */, ); path = Views; sourceTree = ""; @@ -472,12 +426,14 @@ AB0BD3922AE7071200EB51AF /* Previews */ = { isa = PBXGroup; children = ( - AB0BD3932AE7071200EB51AF /* PreviewsStationsModal.swift */, - AB0BD3942AE7071200EB51AF /* PreviewsDataiOS.swift */, - AB0BD3952AE7071200EB51AF /* PreviewsTapOverlay.swift */, - AB0BD3962AE7071200EB51AF /* PreviewsAppStartup.swift */, - AB0BD3972AE7071200EB51AF /* PreviewsAppResult.swift */, - AB0BD3982AE7071200EB51AF /* PreviewsAppRunning.swift */, + AB3095E52D2318A400955598 /* Previews.swift */, + AB3095E62D2318A400955598 /* PreviewsHelpers.swift */, + AB3095E72D2318A400955598 /* PreviewsLoadingStates.swift */, + AB3095E82D2318A400955598 /* PreviewsSidebar.swift */, + AB96C8AE2D4EA972007B493C /* PreviewsStates.swift */, + AB3095EA2D2318A400955598 /* PreviewsStationView.swift */, + AB3095EB2D2318A400955598 /* PreviewsStationView_EdgeCases.swift */, + AB3095AD2D23177300955598 /* PreviewsData.swift */, ); path = Previews; sourceTree = ""; @@ -511,9 +467,58 @@ path = LuasWatchComplications; sourceTree = ""; }; + AB3095BC2D23182700955598 /* Sidebar */ = { + isa = PBXGroup; + children = ( + AB3095B32D23182700955598 /* AllStationsListView.swift */, + AB3095B42D23182700955598 /* FavouritesHeaderView.swift */, + AB3095B52D23182700955598 /* FavouritesSidebarView.swift */, + AB3095B62D23182700955598 /* LineRow.swift */, + AB3095B72D23182700955598 /* LinesView.swift */, + AB3095B82D23182700955598 /* NearbyStationsView.swift */, + AB3095B92D23182700955598 /* SidebarView.swift */, + AB3095BA2D23182700955598 /* StationRow.swift */, + AB3095BB2D23182700955598 /* StationsModal.swift */, + ); + path = Sidebar; + sourceTree = ""; + }; + AB3095D52D23186C00955598 /* DetailView */ = { + isa = PBXGroup; + children = ( + AB3095C72D23186C00955598 /* DoubleTimetableView.swift */, + AB3095C82D23186C00955598 /* DueView.swift */, + AB3095C92D23186C00955598 /* GrantLocationAuthView.swift */, + AB3095CA2D23186C00955598 /* LuasTextView.swift */, + AB3095CB2D23186C00955598 /* NoTrainsView.swift */, + AB3095CC2D23186C00955598 /* OverflowDotsView.swift */, + AB3095CD2D23186C00955598 /* SimpleTimetableView.swift */, + AB3095CE2D23186C00955598 /* StationTimesView.swift */, + AB96C8B02D502915007B493C /* ClosestStationsView.swift */, + AB3095CF2D23186C00955598 /* StationToolbar.swift */, + AB3095D02D23186C00955598 /* StationToolbarLoading.swift */, + AB3095D12D23186C00955598 /* StationView.swift */, + AB3095D22D23186C00955598 /* ToolbarInactive.swift */, + AB3095D32D23186C00955598 /* TrainsViewLoading.swift */, + AB3095D42D23186C00955598 /* View+LuasFormatting.swift */, + ); + path = DetailView; + sourceTree = ""; + }; + AB3095F62D2318E200955598 /* Models */ = { + isa = PBXGroup; + children = ( + AB3095F32D2318E200955598 /* FavouriteStation.swift */, + AB3095F42D2318E200955598 /* ModelContext+Extensions.swift */, + AB3095F52D2318E200955598 /* StationDirection.swift */, + ); + path = Models; + sourceTree = ""; + }; AB364CF82308276C00C73EF6 = { isa = PBXGroup; children = ( + AB50A2082D4C26F0002A2DB8 /* README.md */, AB3095A42D23039A00955598 /* LuasAppTestPlan.xctestplan */, AB79B5802D230098001E20F8 /* LuasAPITestPlan.xctestplan */, AB79B57B2D219B9E001E20F8 /* LuasApp */, @@ -521,17 +526,14 @@ AB387B402CBBE202002D4B4A /* myConfiguration.json */, AB925F442B6FAE5700F23497 /* LuasWatch Watch App.xctestplan */, ABAFF5B424ED58A8004DB119 /* LuasPlayground.playground */, - AB57F0DE230B3C1900BA8B53 /* LuasKit */, AB695E312AE41C1D00B6DB99 /* LuasWatch Watch App */, AB695E422AE41C1F00B6DB99 /* LuasWatch Watch AppTests */, AB695E4C2AE41C1F00B6DB99 /* LuasWatch Watch AppUITests */, AB2453F62AE424B40007FA7C /* LuasWatchComplications */, - AB0BD3482AE6F61700EB51AF /* LuasKitTests */, AB0BD3612AE7062300EB51AF /* LuasWatchiOS */, AB0BD3722AE7062400EB51AF /* LuasWatchiOSTests */, AB0BD37C2AE7062400EB51AF /* LuasWatchiOSUITests */, AB364D002308276C00C73EF6 /* Products */, - AB57F129230B432F00BA8B53 /* LuasKitIOS-Info.plist */, AB15B8772686815E005BF013 /* Frameworks */, ); sourceTree = ""; @@ -539,13 +541,11 @@ AB364D002308276C00C73EF6 /* Products */ = { isa = PBXGroup; children = ( - AB57F0DD230B3C1900BA8B53 /* LuasKit.framework */, AB695E282AE41C1C00B6DB99 /* LuasWatch.app */, AB695E2D2AE41C1C00B6DB99 /* LuasWatch Watch App.app */, AB695E3F2AE41C1E00B6DB99 /* LuasWatch Watch AppTests.xctest */, AB695E492AE41C1F00B6DB99 /* LuasWatch Watch AppUITests.xctest */, AB2453F12AE424B40007FA7C /* LuasWatchComplicationsExtension.appex */, - AB0BD3472AE6F61700EB51AF /* LuasKitTests.xctest */, AB0BD3602AE7062200EB51AF /* LuasWatchiOS.app */, AB0BD36F2AE7062400EB51AF /* LuasWatchiOSTests.xctest */, AB0BD3792AE7062400EB51AF /* LuasWatchiOSUITests.xctest */, @@ -553,53 +553,6 @@ name = Products; sourceTree = ""; }; - AB3CC0072AE4094F002AA60A /* API */ = { - isa = PBXGroup; - children = ( - AB3CC00A2AE4094F002AA60A /* API.swift */, - AB3CC0092AE4094F002AA60A /* LuasMockAPI.swift */, - AB3CC0082AE4094F002AA60A /* APIParser.swift */, - AB3CC00B2AE4094F002AA60A /* Parser.swift */, - ); - path = API; - sourceTree = ""; - }; - AB3CC0112AE4094F002AA60A /* Models */ = { - isa = PBXGroup; - children = ( - AB3CC0162AE4094F002AA60A /* Route.swift */, - AB3CC0142AE4094F002AA60A /* Train.swift */, - AB3CC0132AE4094F002AA60A /* TrainStation.swift */, - AB3CC0152AE4094F002AA60A /* TrainStations.swift */, - AB3CC0172AE4094F002AA60A /* Direction.swift */, - AB3CC0122AE4094F002AA60A /* TrainsByDirection.swift */, - ); - path = Models; - sourceTree = ""; - }; - AB57F0DE230B3C1900BA8B53 /* LuasKit */ = { - isa = PBXGroup; - children = ( - AB3CC01A2AE4094F002AA60A /* LuasKit.h */, - AB3CC0112AE4094F002AA60A /* Models */, - AB3CC0072AE4094F002AA60A /* API */, - ABC24A7E2C0525F50004014B /* AppState.swift */, - ABC24A802C0526950004014B /* AppMode.swift */, - AB8220782B49F79200022CA0 /* AppModel.swift */, - AB925F4A2B715FAD00F23497 /* AppModel+Codable.swift */, - AB925F4C2B715FCD00F23497 /* AppModel+CustomStringConvertible.swift */, - AB3CC00E2AE4094F002AA60A /* Location.swift */, - ABC207AC2B8156BB00C1F8F7 /* CLAuthorizationStatus+Extensions.swift */, - AB3CC00D2AE4094F002AA60A /* luasStops.json */, - AB3CC0102AE4094F002AA60A /* LuasStrings.swift */, - AB3CC0192AE4094F002AA60A /* LuasExtensions.swift */, - AB3CC00F2AE4094F002AA60A /* Logging.swift */, - AB3CC0182AE4094F002AA60A /* MyUserDefaults.swift */, - AB57F0E0230B3C1900BA8B53 /* Info.plist */, - ); - path = LuasKit; - sourceTree = ""; - }; AB695E312AE41C1D00B6DB99 /* LuasWatch Watch App */ = { isa = PBXGroup; children = ( @@ -708,37 +661,7 @@ }; /* End PBXGroup section */ -/* Begin PBXHeadersBuildPhase section */ - AB57F0D8230B3C1900BA8B53 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - AB3CC02C2AE4094F002AA60A /* LuasKit.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - /* Begin PBXNativeTarget section */ - AB0BD3462AE6F61700EB51AF /* LuasKitTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = AB0BD3502AE6F61700EB51AF /* Build configuration list for PBXNativeTarget "LuasKitTests" */; - buildPhases = ( - AB925F432B6FAD7800F23497 /* Run swift-format */, - AB0BD3432AE6F61700EB51AF /* Sources */, - AB0BD3442AE6F61700EB51AF /* Frameworks */, - AB0BD3452AE6F61700EB51AF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - AB0BD34D2AE6F61700EB51AF /* PBXTargetDependency */, - ); - name = LuasKitTests; - productName = LuasKitTests; - productReference = AB0BD3472AE6F61700EB51AF /* LuasKitTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; AB0BD35F2AE7062200EB51AF /* LuasWatchiOS */ = { isa = PBXNativeTarget; buildConfigurationList = AB0BD3812AE7062400EB51AF /* Build configuration list for PBXNativeTarget "LuasWatchiOS" */; @@ -746,7 +669,6 @@ AB0BD35C2AE7062200EB51AF /* Sources */, AB0BD35D2AE7062200EB51AF /* Frameworks */, AB0BD35E2AE7062200EB51AF /* Resources */, - AB0BD3AD2AE70AFD00EB51AF /* Embed Frameworks */, ); buildRules = ( ); @@ -810,24 +732,6 @@ productReference = AB2453F12AE424B40007FA7C /* LuasWatchComplicationsExtension.appex */; productType = "com.apple.product-type.app-extension"; }; - AB57F0DC230B3C1900BA8B53 /* LuasKit */ = { - isa = PBXNativeTarget; - buildConfigurationList = AB57F0E9230B3C1900BA8B53 /* Build configuration list for PBXNativeTarget "LuasKit" */; - buildPhases = ( - AB57F0D8230B3C1900BA8B53 /* Headers */, - AB57F0D9230B3C1900BA8B53 /* Sources */, - AB57F0DA230B3C1900BA8B53 /* Frameworks */, - AB57F0DB230B3C1900BA8B53 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = LuasKit; - productName = LuasKit; - productReference = AB57F0DD230B3C1900BA8B53 /* LuasKit.framework */; - productType = "com.apple.product-type.framework"; - }; AB695E272AE41C1C00B6DB99 /* LuasWatch */ = { isa = PBXNativeTarget; buildConfigurationList = AB695E552AE41C1F00B6DB99 /* Build configuration list for PBXNativeTarget "LuasWatch" */; @@ -913,9 +817,6 @@ LastUpgradeCheck = 1610; ORGANIZATIONNAME = mApps.ie; TargetAttributes = { - AB0BD3462AE6F61700EB51AF = { - CreatedOnToolsVersion = 15.0; - }; AB0BD35F2AE7062200EB51AF = { CreatedOnToolsVersion = 15.0; }; @@ -930,10 +831,6 @@ AB2453F02AE424B40007FA7C = { CreatedOnToolsVersion = 15.0; }; - AB57F0DC230B3C1900BA8B53 = { - CreatedOnToolsVersion = 11.0; - LastSwiftMigration = 1500; - }; AB695E272AE41C1C00B6DB99 = { CreatedOnToolsVersion = 15.0; }; @@ -965,13 +862,11 @@ projectDirPath = ""; projectRoot = ""; targets = ( - AB57F0DC230B3C1900BA8B53 /* LuasKit */, AB695E2C2AE41C1C00B6DB99 /* LuasWatch Watch App */, AB2453F02AE424B40007FA7C /* LuasWatchComplicationsExtension */, AB695E272AE41C1C00B6DB99 /* LuasWatch */, AB695E3E2AE41C1E00B6DB99 /* LuasWatch Watch AppTests */, AB695E482AE41C1F00B6DB99 /* LuasWatch Watch AppUITests */, - AB0BD3462AE6F61700EB51AF /* LuasKitTests */, AB0BD35F2AE7062200EB51AF /* LuasWatchiOS */, AB0BD36E2AE7062400EB51AF /* LuasWatchiOSTests */, AB0BD3782AE7062400EB51AF /* LuasWatchiOSUITests */, @@ -980,13 +875,6 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - AB0BD3452AE6F61700EB51AF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; AB0BD35E2AE7062200EB51AF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1018,14 +906,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - AB57F0DB230B3C1900BA8B53 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - AB3CC0202AE4094F002AA60A /* luasStops.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; AB695E262AE41C1C00B6DB99 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1059,25 +939,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - AB925F432B6FAD7800F23497 /* Run swift-format */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run swift-format"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export PATH=$PATH:/opt/homebrew/bin/\n\nif which swift-format >/dev/null; then\n swift-format -i --configuration myConfiguration.json --ignore-unparsable-files -p -r LuasKit\nelse\n echo \"warning: swift-format not installed\"\nfi\n"; - }; AB925F452B6FAEB200F23497 /* Run swift-format */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -1100,32 +961,52 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - AB0BD3432AE6F61700EB51AF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - AB0BD35A2AE6F64C00EB51AF /* TestDefinitions.swift in Sources */, - AB0BD3562AE6F64C00EB51AF /* ModelsTests.swift in Sources */, - AB0BD3592AE6F64C00EB51AF /* SnapshotTests.swift in Sources */, - AB0BD3582AE6F64C00EB51AF /* APITests.swift in Sources */, - AB0BD3572AE6F64C00EB51AF /* ParserTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; AB0BD35C2AE7062200EB51AF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AB0BD39E2AE7071200EB51AF /* ChangeStationButton.swift in Sources */, - AB0BD3A02AE7071200EB51AF /* LuasView.swift in Sources */, AB0BD3A92AE7086700EB51AF /* Coordinator.swift in Sources */, - AB0BD39D2AE7071200EB51AF /* HeaderView.swift in Sources */, - AB0BD39B2AE7071200EB51AF /* LoadingAnimationView.swift in Sources */, - AB0BD39F2AE7071200EB51AF /* TapOverlayView.swift in Sources */, - AB0BD39A2AE7071200EB51AF /* GrantLocationAuthView.swift in Sources */, - AB0BD3A22AE7071200EB51AF /* PreviewsDataiOS.swift in Sources */, + AB3095B02D23179C00955598 /* CLLocation+Extensions.swift in Sources */, + AB3095B22D2317F200955598 /* LuasMainScreen.swift in Sources */, + AB3095AB2D23173200955598 /* Coordinator+LocationDelegate.swift in Sources */, + AB3095AC2D23173200955598 /* Coordinator+Animiation.swift in Sources */, + AB3095F72D2318E200955598 /* ModelContext+Extensions.swift in Sources */, + AB3095F82D2318E200955598 /* FavouriteStation.swift in Sources */, + AB3095F92D2318E200955598 /* StationDirection.swift in Sources */, AB0BD3632AE7062300EB51AF /* LuasWatchiOSApp.swift in Sources */, - AB0BD39C2AE7071200EB51AF /* TrainsListView.swift in Sources */, + AB3095AE2D23177300955598 /* PreviewsData.swift in Sources */, + AB3095BD2D23182700955598 /* AllStationsListView.swift in Sources */, + AB3095BE2D23182700955598 /* FavouritesSidebarView.swift in Sources */, + AB3095BF2D23182700955598 /* StationRow.swift in Sources */, + AB3095C02D23182700955598 /* NearbyStationsView.swift in Sources */, + AB3095C12D23182700955598 /* StationsModal.swift in Sources */, + AB3095C22D23182700955598 /* LineRow.swift in Sources */, + AB3095C32D23182700955598 /* LinesView.swift in Sources */, + AB3095D62D23186C00955598 /* LuasTextView.swift in Sources */, + AB3095D72D23186C00955598 /* TrainsViewLoading.swift in Sources */, + AB96C8AF2D4EA972007B493C /* PreviewsStates.swift in Sources */, + AB3095D82D23186C00955598 /* SimpleTimetableView.swift in Sources */, + AB3095D92D23186C00955598 /* StationView.swift in Sources */, + AB3095DA2D23186C00955598 /* StationToolbar.swift in Sources */, + AB3095DB2D23186C00955598 /* OverflowDotsView.swift in Sources */, + AB3095DC2D23186C00955598 /* GrantLocationAuthView.swift in Sources */, + AB3095DD2D23186C00955598 /* StationTimesView.swift in Sources */, + AB3095DE2D23186C00955598 /* View+LuasFormatting.swift in Sources */, + AB3095DF2D23186C00955598 /* Collection+Extensions.swift in Sources */, + AB3095E02D23186C00955598 /* StationToolbarLoading.swift in Sources */, + AB3095E12D23186C00955598 /* ToolbarInactive.swift in Sources */, + AB3095E22D23186C00955598 /* DoubleTimetableView.swift in Sources */, + AB3095EC2D2318A400955598 /* PreviewsHelpers.swift in Sources */, + AB3095ED2D2318A400955598 /* PreviewsSidebar.swift in Sources */, + AB96C8B12D502915007B493C /* ClosestStationsView.swift in Sources */, + AB3095EF2D2318A400955598 /* PreviewsStationView.swift in Sources */, + AB3095F02D2318A400955598 /* Previews.swift in Sources */, + AB3095F12D2318A400955598 /* PreviewsLoadingStates.swift in Sources */, + AB3095F22D2318A400955598 /* PreviewsStationView_EdgeCases.swift in Sources */, + AB3095E32D23186C00955598 /* NoTrainsView.swift in Sources */, + AB3095E42D23186C00955598 /* DueView.swift in Sources */, + AB3095C42D23182700955598 /* FavouritesHeaderView.swift in Sources */, + AB3095C52D23182700955598 /* SidebarView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1154,34 +1035,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - AB57F0D9230B3C1900BA8B53 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - AB3CC01E2AE4094F002AA60A /* Parser.swift in Sources */, - AB3CC0272AE4094F002AA60A /* TrainStations.swift in Sources */, - ABC24A812C0526950004014B /* AppMode.swift in Sources */, - AB3CC0252AE4094F002AA60A /* TrainStation.swift in Sources */, - AB3CC0262AE4094F002AA60A /* Train.swift in Sources */, - AB925F4D2B715FCD00F23497 /* AppModel+CustomStringConvertible.swift in Sources */, - AB3CC02B2AE4094F002AA60A /* LuasExtensions.swift in Sources */, - AB3CC0282AE4094F002AA60A /* Route.swift in Sources */, - ABC207AD2B8156BB00C1F8F7 /* CLAuthorizationStatus+Extensions.swift in Sources */, - AB3CC02A2AE4094F002AA60A /* MyUserDefaults.swift in Sources */, - AB3CC0242AE4094F002AA60A /* TrainsByDirection.swift in Sources */, - AB3CC0292AE4094F002AA60A /* Direction.swift in Sources */, - ABC24A7F2C0525F50004014B /* AppState.swift in Sources */, - AB8220792B49F79200022CA0 /* AppModel.swift in Sources */, - AB3CC01B2AE4094F002AA60A /* APIParser.swift in Sources */, - AB925F4B2B715FAD00F23497 /* AppModel+Codable.swift in Sources */, - AB3CC0232AE4094F002AA60A /* LuasStrings.swift in Sources */, - AB3CC0222AE4094F002AA60A /* Logging.swift in Sources */, - AB3CC01D2AE4094F002AA60A /* API.swift in Sources */, - AB3CC0212AE4094F002AA60A /* Location.swift in Sources */, - AB3CC01C2AE4094F002AA60A /* LuasMockAPI.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; AB695E292AE41C1C00B6DB99 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1251,11 +1104,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - AB0BD34D2AE6F61700EB51AF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = AB57F0DC230B3C1900BA8B53 /* LuasKit */; - targetProxy = AB0BD34C2AE6F61700EB51AF /* PBXContainerItemProxy */; - }; AB0BD3712AE7062400EB51AF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AB0BD35F2AE7062200EB51AF /* LuasWatchiOS */; @@ -1289,47 +1137,6 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - AB0BD34E2AE6F61700EB51AF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 2H43Y8J8SC; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Debug; - }; - AB0BD34F2AE6F61700EB51AF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 2H43Y8J8SC; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Release; - }; AB0BD3822AE7062400EB51AF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1508,6 +1315,7 @@ "@executable_path/../../../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasWatch.watchkitapp.LuasWatchComplications; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -1539,6 +1347,7 @@ "@executable_path/../../../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasWatch.watchkitapp.LuasWatchComplications; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -1583,9 +1392,9 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEBUG_INFORMATION_FORMAT = dwarf; - DYLIB_CURRENT_VERSION = 125; + DYLIB_CURRENT_VERSION = 126; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1651,9 +1460,9 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_CURRENT_VERSION = 125; + DYLIB_CURRENT_VERSION = 126; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1678,72 +1487,6 @@ }; name = Release; }; - AB57F0E7230B3C1900BA8B53 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 2H43Y8J8SC; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = LuasKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; - PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasKit; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator watchos watchsimulator"; - SUPPORTS_MACCATALYST = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TARGETED_DEVICE_FAMILY = "1,2,4"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - AB57F0E8230B3C1900BA8B53 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 2H43Y8J8SC; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; - INFOPLIST_FILE = LuasKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; - PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasKit; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator watchos watchsimulator"; - SUPPORTS_MACCATALYST = NO; - TARGETED_DEVICE_FAMILY = "1,2,4"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; AB695E522AE41C1F00B6DB99 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1770,6 +1513,7 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasWatch.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -1806,6 +1550,7 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasWatch.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -1825,6 +1570,7 @@ GCC_C_LANGUAGE_STANDARD = gnu17; INFOPLIST_KEY_CFBundleDisplayName = LuasWatch; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasWatch; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; @@ -1842,6 +1588,7 @@ GCC_C_LANGUAGE_STANDARD = gnu17; INFOPLIST_KEY_CFBundleDisplayName = LuasWatch; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = ie.mapps.LuasWatch; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1937,15 +1684,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - AB0BD3502AE6F61700EB51AF /* Build configuration list for PBXNativeTarget "LuasKitTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - AB0BD34E2AE6F61700EB51AF /* Debug */, - AB0BD34F2AE6F61700EB51AF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; AB0BD3812AE7062400EB51AF /* Build configuration list for PBXNativeTarget "LuasWatchiOS" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1991,15 +1729,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - AB57F0E9230B3C1900BA8B53 /* Build configuration list for PBXNativeTarget "LuasKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - AB57F0E7230B3C1900BA8B53 /* Debug */, - AB57F0E8230B3C1900BA8B53 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; AB695E512AE41C1F00B6DB99 /* Build configuration list for PBXNativeTarget "LuasWatch Watch App" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2039,6 +1768,14 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + AB3095A52D2315E100955598 /* LuasAPI */ = { + isa = XCSwiftPackageProductDependency; + productName = LuasAPI; + }; + AB3095A72D2315E100955598 /* LuasApp */ = { + isa = XCSwiftPackageProductDependency; + productName = LuasApp; + }; AB79B57C2D21A614001E20F8 /* LuasAPI */ = { isa = XCSwiftPackageProductDependency; productName = LuasAPI; diff --git a/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasKitTests.xcscheme b/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasKitTests.xcscheme deleted file mode 100644 index 66c3188..0000000 --- a/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasKitTests.xcscheme +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasKit.xcscheme b/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasWatch.xcscheme similarity index 65% rename from LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasKit.xcscheme rename to LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasWatch.xcscheme index 0ed8a6b..1fc3535 100644 --- a/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasKit.xcscheme +++ b/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasWatch.xcscheme @@ -4,7 +4,8 @@ version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> @@ -26,13 +27,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + + + + - + - + diff --git a/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasWatchiOS.xcscheme b/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasWatchiOS.xcscheme new file mode 100644 index 0000000..dc1e636 --- /dev/null +++ b/LuasWatch.xcodeproj/xcshareddata/xcschemes/LuasWatchiOS.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LuasWatchiOS/Assets.xcassets/luasGreen.colorset/Contents.json b/LuasWatchiOS/Assets.xcassets/luasGreen.colorset/Contents.json new file mode 100644 index 0000000..1d365ed --- /dev/null +++ b/LuasWatchiOS/Assets.xcassets/luasGreen.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x65", + "green" : "0xA6", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x65", + "green" : "0xA6", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LuasWatchiOS/Assets.xcassets/luasRed.colorset/Contents.json b/LuasWatchiOS/Assets.xcassets/luasRed.colorset/Contents.json new file mode 100644 index 0000000..7eb63b5 --- /dev/null +++ b/LuasWatchiOS/Assets.xcassets/luasRed.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x56", + "green" : "0x47", + "red" : "0xF0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x56", + "green" : "0x47", + "red" : "0xF0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LuasWatchiOS/Assets.xcassets/luasTheme.colorset/Contents.json b/LuasWatchiOS/Assets.xcassets/luasTheme.colorset/Contents.json new file mode 100644 index 0000000..9530078 --- /dev/null +++ b/LuasWatchiOS/Assets.xcassets/luasTheme.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xB6", + "green" : "0x46", + "red" : "0x5B" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xB6", + "green" : "0x46", + "red" : "0x5B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LuasWatchiOS/CLLocation+Extensions.swift b/LuasWatchiOS/CLLocation+Extensions.swift new file mode 100644 index 0000000..b150386 --- /dev/null +++ b/LuasWatchiOS/CLLocation+Extensions.swift @@ -0,0 +1,32 @@ +// +// Created by Roland Gropmair on 13/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import CoreLocation + +extension CLLocation { + + func isQuiteRecent() -> Bool { + timestamp.timeIntervalSinceNow > -20.0 + } +} + +extension CLAuthorizationStatus { + + func localizedErrorMessage() -> String? { + switch self { + case .notDetermined: + return NSLocalizedString("auth status not determined (yet)", comment: "") + + case .restricted: + return NSLocalizedString("auth status restricted", comment: "") + + case .denied: + return NSLocalizedString("auth status denied", comment: "") + + default: + return nil + } + } +} diff --git a/LuasWatchiOS/Coordinator+Animiation.swift b/LuasWatchiOS/Coordinator+Animiation.swift new file mode 100644 index 0000000..0b0844b --- /dev/null +++ b/LuasWatchiOS/Coordinator+Animiation.swift @@ -0,0 +1,24 @@ +// +// Created by Roland Gropmair on 17/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +extension Coordinator { + + func updateWithAnimation(to state: AppState) { + + withAnimation { + if Thread.isMainThread { + appModel.appState = state + } else { + DispatchQueue.main.async { [weak self] in + self?.appModel.appState = state + } + } + } + } +} diff --git a/LuasWatchiOS/Coordinator+LocationDelegate.swift b/LuasWatchiOS/Coordinator+LocationDelegate.swift new file mode 100644 index 0000000..b709cdb --- /dev/null +++ b/LuasWatchiOS/Coordinator+LocationDelegate.swift @@ -0,0 +1,187 @@ +// +// Created by Roland Gropmair on 13/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import CoreLocation +import LuasAPI +import LuasApp + +extension Coordinator: LocationDelegate { + + func didFail(_ delegateError: LocationDelegateError) { + myPrint("error \(delegateError)") + + appModel.latestLocation = nil + + switch delegateError { + + case .locationServicesNotEnabled: + appModel.locationDenied = true + + updateWithAnimation(to: .errorGettingLocation(LuasStrings.locationServicesDisabled)) + + case .locationAccessDenied: + appModel.locationDenied = true + + // that does not show up -> do I need to set appModel.selectedStation = + updateWithAnimation(to: .errorGettingLocation(LuasStrings.locationAccessDenied)) + + case .locationManagerError(let error): + updateWithAnimation(to: .errorGettingLocation(error.localizedDescription)) + } + } + + func didEnableLocation() { + appModel.locationDenied = false + + #if DEBUG +// if isRunningUnitTests() { return } + #endif + + if appModel.appMode.needsLocation { + location.start() + } else { + myPrint("no location auth needed for the current appMode \(appModel.appMode)") + } + } + + func didGetLocation(_ location: CLLocation) { + + appModel.locationDenied = false + appModel.latestLocation = location + + // ////////////////////////////////////////////// + // step 2: we have location -> now find station + let allStations = TrainStations() + + if let station = appModel.appMode.specificStation { + myPrint( + "step 2a: got location now, but user selected specific station before -> use this station now" + ) + handle(station) + + } else { + myPrint("step 2b: got location; find closest station (no matter which line)") + + if let closestStation = allStations.closestStation(from: location) { + + if appModel.appMode == .closest { + myPrint("found closest station <\(closestStation.name)>") + handle(closestStation) + } else if appModel.appMode == .closestOtherLine, + let closestOtherLine = allStations.closestStation( + from: location, route: closestStation.route.other) + { + myPrint("found closest other line station <\(closestOtherLine.name)>") + handle(closestOtherLine) + } else { + assertionFailure("internal error") + // this should not happen unless we missed an appMode case; as fallback let's use the closestStation + myPrint("found closest station <\(closestStation.name)>") + handle(closestStation) + } + + } else { + myPrint("step 2c: no station found -> user too far away") + + guard appModel.allowStationTabviewUpdates == true else { + myPrint("SidebarView is up -> ignore new location so we don't interfere UI") + return + } + + updateWithAnimation(to: .errorGettingStationTooFarAway(LuasStrings.tooFarAway)) + } + } + } + + internal func handle( + _ closestStation: TrainStation + ) { + appModel.selectedStation = closestStation + + if appModel.allowStationTabviewUpdates { + if let cachedTrains = previouslyLoadedTrains, + cachedTrains.for.name == closestStation.name + { + /// only use the cached trains list if they actually match the station we're about to load + /// (otherwise the UI looks wrong, e.g. might show the incorrect line color + + /// DON'T use updateWithAnimation() here, at first launch it shows empty content?!? + appModel.appState = .loadingDueTimes( + closestStation, cachedTrains: cachedTrains.trains) + } else { + + /// DON'T use updateWithAnimation() here, at first launch it shows empty content?!? + appModel.appState = .loadingDueTimes( + closestStation, cachedTrains: nil) + } + } else { + myPrint("SidebarView is up -> ignore so we don't interfere UI") + } + + // ////////////////////////////////////////////// + // step 3: get due times from API + Task { + + do { + + myPrint("calling API for station \(closestStation.name) ...") + + let trains = try await self.api.dueTimes(for: closestStation) + + myPrint("... got trains \(trains)") + + previouslyLoadedTrains = (for: closestStation, trains: trains) + if appModel.allowStationTabviewUpdates { + updateWithAnimation(to: .foundDueTimes(trains)) + } else { + myPrint("SidebarView is up -> ignore so we don't interfere UI") + } + + } catch { + + myPrint("...caught error \(error.localizedDescription)") + + previouslyLoadedTrains = nil + + guard appModel.allowStationTabviewUpdates else { + myPrint("SidebarView is up -> ignore so we don't interfere UI") + return + } + + if let apiError = error as? APIError { + + switch apiError { + case .noTrainsButMessageFromAPI(let message): + updateWithAnimation(to: + .errorGettingDueTimes( + closestStation, + message)) + + case .noTrains: + updateWithAnimation(to: + .errorGettingDueTimes( + closestStation, + LuasStrings.noTrainsErrorMessage)) + + case .invalidXML: + updateWithAnimation( + to: .errorGettingDueTimes(closestStation, "Error reading server response")) + } + + } else if (error as NSError).code == NSURLErrorNotConnectedToInternet { + updateWithAnimation( + to: .errorGettingDueTimes( + closestStation, + LuasStrings.errorNoInternet)) + } else { + updateWithAnimation( + to: .errorGettingDueTimes( + closestStation, + LuasStrings.errorGettingDueTimes(station: closestStation.name))) + } + } + } + } +} diff --git a/LuasWatchiOS/Coordinator.swift b/LuasWatchiOS/Coordinator.swift index 3b24efe..157fa99 100644 --- a/LuasWatchiOS/Coordinator.swift +++ b/LuasWatchiOS/Coordinator.swift @@ -3,45 +3,96 @@ // Copyright © 2019 mApps.ie. All rights reserved. // -import CoreLocation -import LuasKit +import Combine +import Foundation +import LuasAPI +import LuasApp class Coordinator: NSObject { - private let api = LuasAPI(apiWorker: RealAPIWorker()) + internal let appModel: AppModel + internal var location: Location + internal let api = LuasAPI(session: URLSession.shared) - private let appState: AppState - private var location: Location private var timer: Timer? + private static let refreshInterval = 12.0 + private var cancellable: AnyCancellable? - private var latestLocation: CLLocation? - - private var trains: TrainsByDirection? - - static let refreshInterval = 12.0 + internal var previouslyLoadedTrains: (for: TrainStation, trains: TrainsByDirection)? init( - appState: AppState, + appModel: AppModel, location: Location ) { - self.appState = appState + self.appModel = appModel self.location = location + self.cancellable = appModel.$appState + .sink { newAppState in + if case .gettingLocation = newAppState { + location.promptLocationAuth() + } + } + } + + deinit { + // WIP do we actually need to do that manually? + cancellable?.cancel() } func start() { - ////////////////////////////////// - // step 1: determine location + #if DEBUG + if appModel.mockMode == true { + // force specific app flow for debugging and taking screenshots + + appModel.appState = .foundDueTimes(trainsGreen) + + // testing: switch to another state with delay + // appModel.appState = .gettingLocation + // + // executeAfterDelay { [weak self] in + // self?.appModel.appState = .errorGettingLocation("error getting location") + // + // self?.executeAfterDelay { + // self?.appModel.appState = .locationAuthorizationUnknown + // } + // } + return + } + #endif + + // ////////////////////////////////////////////// + // step 1: if required, determine location location.delegate = self - // dont call start() here anymore - we call it once user has authorized location access - // location.start() + if appModel.appMode.needsLocation { + + myPrint( + "need location auth for current appMode \(appModel.appMode) -> prompt for location auth" + ) + location.promptLocationAuth() + /// we will call location.start() once user has authorized location access + + } else { + myPrint( + "no location auth needed for the current appMode \(appModel.appMode)") + + /// don't call handle here -> because when app goes to active`fireAndScheduleTimer` will be called by changing of the scenePhase + // guard let specificStation = appModel.appMode.specificStation else { + // assertionFailure("internal error") + // myPrint("🚨 internal error: expected specific station in appModel") + // return + // } + // handle(specificStation) + } + + #warning("notification is sent by appMode.didSet - is there a better way?") NotificationCenter.default.addObserver( forName: Notification.Name("LuasWatch.RetriggerTimer"), object: nil, queue: nil ) { _ in - self.retriggerTimer() + self.fireAndScheduleTimer() } } @@ -49,24 +100,23 @@ class Coordinator: NSObject { timer?.invalidate() } - func scheduleTimer() { - // fire right now... - timerDidFire() + func fireAndScheduleTimer() { + myPrint(#function) - // ... but also schedule for later - timer = Timer.scheduledTimer( - timeInterval: Self.refreshInterval, - target: self, selector: #selector(timerDidFire), - userInfo: nil, repeats: true) - } + invalidateTimer() - func retriggerTimer() { - timer?.invalidate() + /// when we tap a station in sidebarView and force a retrigger, it's still up & we would ignore it -> let's override this check + appModel.allowStationTabviewUpdates = true - // fire right now... + // fire and schedule timerDidFire() + scheduleTimer() + } + + // schedule timer for regular interval + internal func scheduleTimer() { + myPrint("\(#function)") - // ... and then schedule again for regular interval timer = Timer.scheduledTimer( timeInterval: Self.refreshInterval, target: self, selector: #selector(timerDidFire), @@ -74,196 +124,44 @@ class Coordinator: NSObject { } @objc func timerDidFire() { + myPrint("\(#function)") - guard appState.isStationsModalPresented == false else { + guard appModel.allowStationTabviewUpdates == true else { myPrint( - "💔 StationsModal is up (isStationsModalPresented == true) -> ignore location update timer") + "SidebarView is up -> ignore timer firing so we don't interfere UI") return } - // if user has selected a specific station - if let station = MyUserDefaults.userSelectedSpecificStation() { + if let station = appModel.appMode.specificStation { - // the location we have is not too old -> don't wait for another location update - if let latestLocation, - latestLocation.isQuiteRecent() - { - myPrint("🥳 we have user selected station & recent location -> skip location update") - handle(station, latestLocation) - } else { - myPrint( - "😇 user has selected specific station & only outdated or no location \(latestLocation?.timestamp.timeIntervalSinceNow ?? 0) -> wait for location update" - ) - location.update() - } + myPrint("User selected station -> skip location update") + handle(station) - } else if MyUserDefaults.userSelectedSpecificStation() == nil { - // user has NOT selected a specific station + } else { + + // User has NOT selected a specific station - if let latestLocation = latestLocation, + if let latestLocation = appModel.latestLocation, latestLocation.isQuiteRecent() { - // we have a location that's not too old myPrint( - "🥳 user has NOT selected specific station & we have a recent location -> skip location update" + "User has NOT selected specific station & we have a recent location -> skip location update" ) didGetLocation(latestLocation) } else { myPrint( - "😇 user has NOT selected specific station & only outdated location \(latestLocation?.timestamp.timeIntervalSinceNow ?? 0) -> wait for location update" + "User has NOT selected specific station & only outdated location \(appModel.latestLocation?.timestamp.timeIntervalSinceNow ?? 0) -> wait for location update" ) location.update() } } } -} - -extension CLLocation { - - func isQuiteRecent() -> Bool { - timestamp.timeIntervalSinceNow > -20.0 - } -} - -extension CLAuthorizationStatus { - - func localizedErrorMessage() -> String? { - switch self { - case .notDetermined: - return NSLocalizedString("auth status not determined (yet)", comment: "") - - case .restricted: - return NSLocalizedString("auth status restricted", comment: "") - - case .denied: - return NSLocalizedString("auth status denied", comment: "") - - default: - return nil - } - } -} -extension Coordinator: LocationDelegate { - - func didFail(_ delegateError: LocationDelegateError) { - - latestLocation = nil - - switch delegateError { - - case .locationServicesNotEnabled: - appState.updateWithAnimation(to: .errorGettingLocation(LuasStrings.locationServicesDisabled)) - - case .locationAccessDenied: - appState.updateWithAnimation(to: .errorGettingLocation(LuasStrings.locationAccessDenied)) - - case .locationManagerError(let error): - appState.updateWithAnimation(to: .errorGettingLocation(error.localizedDescription)) - - case .authStatus(let authStatusError): - if let errorMessage = authStatusError.localizedErrorMessage() { - appState.updateWithAnimation( - to: .errorGettingLocation(LuasStrings.gettingLocationAuthError(errorMessage))) - } else { - appState.updateWithAnimation( - to: .errorGettingLocation(LuasStrings.gettingLocationOtherError)) - } - } - } - - func didEnableLocation() { - location.start() - } - - func didGetLocation(_ location: CLLocation) { - - latestLocation = location - - ////////////////////////////////// - // step 2: we have location -> now find station - let allStations = TrainStations.sharedFromFile - - if let station = MyUserDefaults.userSelectedSpecificStation() { - myPrint("step 2a: closest station, but specific one user selected before") - handle(station, location) - - } else { - myPrint("step 2b: closest station, doesn't matter which line") - if let closestStation = allStations.closestStation(from: location) { - myPrint("found closest station <\(closestStation.name)>") - handle(closestStation, location) - } else { - - // no station found -> user too far away! - trains = nil - appState.updateWithAnimation(to: .errorGettingStation(LuasStrings.tooFarAway)) + #if DEBUG + private func executeAfterDelay(_ block: @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + block() } } - - } - - fileprivate func handle( - _ closestStation: TrainStation, - _ location: CLLocation - ) { - // use different states: if we have previously loaded a list of trains, let's preserve it in the UI while loading - - // sometimes crash on watchOS 9 - // [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior - // DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - - if let trains = self.trains { - appState.updateWithAnimation(to: .updatingDueTimes(trains, location)) - } else { - appState.updateWithAnimation(to: .gettingDueTimes(closestStation, location)) - } - - ////////////////////////////////// - // step 3: get due times from API - Task { - - do { - let trains = try await self.api.dueTimes(for: closestStation) - - myPrint("got trains \(trains)") - self.trains = trains - appState.updateWithAnimation(to: .foundDueTimes(trains, location)) - - } catch { - - trains = nil - myPrint("caught error \(error.localizedDescription)") - - if let apiError = error as? APIError { - - switch apiError { - case .noTrains(let message): - appState.updateWithAnimation( - to: - .errorGettingDueTimes( - closestStation, - message.count > 0 ? message : LuasStrings.errorGettingDueTimes)) - - case .invalidXML: - appState.updateWithAnimation( - to: .errorGettingDueTimes(closestStation, "Error reading server response")) - } - } else { - appState.updateWithAnimation( - to: - .errorGettingDueTimes(closestStation, LuasStrings.errorGettingDueTimes)) - } - } - } - } -} - -extension Coordinator: AppStateChangeable { - - func didChange(to state: MyState) { - if case .gettingLocation = state { - location.promptLocationAuth() - } - } + #endif } diff --git a/LuasWatchiOS/LuasWatchiOSApp.swift b/LuasWatchiOS/LuasWatchiOSApp.swift index b4e8bb9..47643ff 100644 --- a/LuasWatchiOS/LuasWatchiOSApp.swift +++ b/LuasWatchiOS/LuasWatchiOSApp.swift @@ -3,28 +3,53 @@ // Copyright © 2023 mApps.ie. All rights reserved. // -import LuasKit import SwiftUI +import SwiftData + +import LuasAPI +import LuasApp @main struct LuasWatchiOSApp: App { @Environment(\.scenePhase) var scenePhase - let appState = AppState() + private var sharedModelContainer: ModelContainer = { + let schema = Schema([ + FavouriteStation.self, + StationDirection.self, + ]) + let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + + do { + let container = try ModelContainer(for: schema, configurations: [modelConfiguration]) + + // WIP create sample data + + return container + } catch { + fatalError("Could not create ModelContainer: \(error)") + } + }() + + let appModel = AppModel() let location = Location() var mainCoordinator: Coordinator! init() { - mainCoordinator = Coordinator(appState: appState, location: location) - appState.changeable = mainCoordinator + mainCoordinator = Coordinator( + appModel: appModel, + location: location) + mainCoordinator.start() } var body: some Scene { WindowGroup { - LuasView() - .environmentObject(appState) + LuasMainScreen() } + .environmentObject(appModel) + .modelContainer(sharedModelContainer) + .onChange(of: scenePhase) { switch $0 { case .background, .inactive: @@ -37,6 +62,5 @@ struct LuasWatchiOSApp: App { break } } - } } diff --git a/LuasWatchiOS/Models/FavouriteStation.swift b/LuasWatchiOS/Models/FavouriteStation.swift new file mode 100644 index 0000000..e5180b1 --- /dev/null +++ b/LuasWatchiOS/Models/FavouriteStation.swift @@ -0,0 +1,25 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import Foundation +import SwiftData + +typealias StationShortCode = String + +@Model +final class FavouriteStation: CustomDebugStringConvertible { + + var shortCode: StationShortCode + var dateAdded: Date + + public init(shortCode: StationShortCode) { + self.shortCode = shortCode + self.dateAdded = Date() + } + + var debugDescription: String { + "Favourite Station: shortCode \"\(shortCode)\"" + } +} diff --git a/LuasWatchiOS/Models/ModelContext+Extensions.swift b/LuasWatchiOS/Models/ModelContext+Extensions.swift new file mode 100644 index 0000000..ca3246c --- /dev/null +++ b/LuasWatchiOS/Models/ModelContext+Extensions.swift @@ -0,0 +1,74 @@ +// +// Created by Roland Gropmair on 04/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftData +import SwiftUI + +extension ModelContext { + + func doesFavouriteStationExist(shortCode: String) -> Bool { + + favouriteStation(shortCode: shortCode) != nil + } + + func favouriteStation(shortCode: String) -> FavouriteStation? { + let descriptor = FetchDescriptor( + predicate: #Predicate { + $0.shortCode == shortCode + }) + + return try? fetch(descriptor).first + } + + func toggleFavouriteStation(shortCode: String) { + + if let favourite = favouriteStation(shortCode: shortCode) { + delete(favourite) + } else { + insert(FavouriteStation(shortCode: shortCode)) + } + } + + internal func direction(for shortCode: String) throws -> [StationDirection] { + + let descriptor = FetchDescriptor( + predicate: #Predicate { + $0.shortCode == shortCode + }) + + return try fetch(descriptor) + } + + func directionConsideringStationType(for shortCode: String) -> Direction { + + guard let station = TrainStations().station(shortCode: shortCode) else { + return .both + } + + if station.isFinalStop || station.stationType == .oneway { + return .both // because we're not sure whether API returns the trains in inbound or outbound array + } else { + + // only now that we checked the stationType, we check the DB + if let obj = try? direction(for: shortCode).first { + return obj.direction + } else { + return .both + } + } + } + + func createOrUpdate(shortCode: String, to direction: Direction) { + + if let station = try? self.direction(for: shortCode).first { + station.direction = direction + return + } + + insert(StationDirection(shortCode: shortCode, direction: direction)) + } +} diff --git a/LuasWatchiOS/Models/StationDirection.swift b/LuasWatchiOS/Models/StationDirection.swift new file mode 100644 index 0000000..2b182bb --- /dev/null +++ b/LuasWatchiOS/Models/StationDirection.swift @@ -0,0 +1,24 @@ +// +// Created by Roland Gropmair on 04/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftData + +@Model +final class StationDirection: CustomDebugStringConvertible { + + var shortCode: StationShortCode + var direction: Direction + + init(shortCode: StationShortCode, direction: Direction) { + self.shortCode = shortCode + self.direction = direction + } + + var debugDescription: String { + "Station Direction: \(direction)" + } +} diff --git a/LuasWatchiOS/Previews/Previews.swift b/LuasWatchiOS/Previews/Previews.swift new file mode 100644 index 0000000..6455bca --- /dev/null +++ b/LuasWatchiOS/Previews/Previews.swift @@ -0,0 +1,58 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import SwiftData + +@MainActor +struct Previews { + let container: ModelContainer + + // NB: need to use mainContext otherwise Previews don't work! + + init(addSample: Bool = true) { + self.init([ + FavouriteStation.self, + StationDirection.self, + ]) + + if addSample { + addSampleData() + } + } + + private init( + _ types: [any PersistentModel.Type], + isStoredInMemoryOnly: Bool = true + ) { + + let schema = Schema(types) + let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: isStoredInMemoryOnly) + self.container = try! ModelContainer(for: schema, configurations: [config]) + } + + mutating func addSampleData() { + _ = FavouriteStation.addPreviews(into: container.mainContext) + } +} + +extension FavouriteStation { + static func addPreviews(into context: ModelContext) -> [FavouriteStation] { + [ + "RAN", + "TAL", + "HOS", + "KIN", + "BEE", + "CCK", + "GAL", + "Invalid", + ] + .map { + let station = FavouriteStation(shortCode: $0) + context.insert(station) + return station + } + } +} diff --git a/LuasWatchiOS/Previews/PreviewsAppResult.swift b/LuasWatchiOS/Previews/PreviewsAppResult.swift deleted file mode 100644 index 3bede6c..0000000 --- a/LuasWatchiOS/Previews/PreviewsAppResult.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// -// -//import LuasKit -//import SwiftUI -// -//#if DEBUG -// // swiftlint:disable:next type_name -// struct Preview_AppResult: PreviewProvider { -// static var previews: some View { -// -// Group { -// LuasMainScreen() -// .environmentObject( -// AppState(state: .foundDueTimes(trainsRed_1_1, userLocation)) -// ) -// .previewDisplayName("found due times - 1:1") -// -// LuasMainScreen() -// .environmentObject( -// AppState(state: .foundDueTimes(trainsRed_1_1, location)) -// ) -// .previewDisplayName("found due times - user very close") -// -// LuasMainScreen() -// .environmentObject( -// AppState(state: .foundDueTimes(trainsRed_2_1, userLocation)) -// ) -// .previewDisplayName("found due times - 2:1") -// -// LuasMainScreen() -// .environmentObject( -// AppState(state: .foundDueTimes(trainsRed_3_2, userLocation)) -// ) -// .previewDisplayName("found due times - 3:2") -// -// LuasMainScreen().previewDevice("Apple Watch Series 3 - 38mm") -// .environmentObject( -// AppState(state: .foundDueTimes(trainsRed_4_4, userLocation)) -// ) -// .previewDisplayName("Small watch - found due times - 4:4") -// -// LuasMainScreen() -// .environmentObject( -// AppState(state: .updatingDueTimes(trainsGreen, userLocation)) -// ) -// .previewDisplayName("updating due times") -// -// LuasMainScreen() -// .environmentObject( -// AppState( -// state: .errorGettingDueTimes( -// stationGreen, -// "No service Broombridge-Parnell. See news.")) -// ) -// .previewDisplayName( -// "error getting due times - with real message from API") -// LuasMainScreen() -// .environmentObject( -// AppState( -// state: .errorGettingDueTimes( -// stationGreen, -// LuasStrings.noTrainsErrorMessage + "\n\n" -// + LuasStrings.noTrainsFallbackExplanation)) -// ) -// .previewDisplayName("error getting due times - with fallback text") -// } -// -// // there is a limit of 10 views we can pack into a group! -// Group { -// LuasMainScreen() -// .environmentObject( -// AppState( -// state: .errorGettingDueTimes( -// stationGreen, -// LuasStrings.noTrainsErrorMessage + "\n\n" -// + LuasStrings.noTrainsFallbackExplanation) -// ) -// ) -// .environment(\.sizeCategory, .extraExtraLarge) -// .previewDisplayName( -// "error getting due times (larger) - not working??") -// -// LuasMainScreen() -// .environmentObject( -// AppState(state: .foundDueTimes(trainsOneWayStation, userLocation)) -// ) -// .previewDisplayName("found due times - one way stop") -// -// LuasMainScreen() -// .environmentObject( -// AppState(state: .foundDueTimes(trainsFinalStop, userLocation)) -// ) -// .previewDisplayName("found due times - final stop") -// } -// } -// } -//#endif diff --git a/LuasWatchiOS/Previews/PreviewsAppRunning.swift b/LuasWatchiOS/Previews/PreviewsAppRunning.swift deleted file mode 100644 index b4f4518..0000000 --- a/LuasWatchiOS/Previews/PreviewsAppRunning.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// - -import SwiftUI - -import LuasAPI -import LuasApp - -#if DEBUG - let genericError = "Some generic error" - - #Preview("while getting info") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .loadingDueTimes( - TrainStation( - stationIdShort: "LUAS70", - shortCode: "CAB", - route: .green, - name: "Cabra", - location: locationBluebell), cachedTrains: nil)) - ) - } - - #Preview("errGetDueTimes (spec)") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingDueTimes(stationRedLongName, genericError)) - ) - } - - #Preview("errGetDueTimes (gen)") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingDueTimes( - stationGreen, - LuasStrings.errorGettingDueTimes(station: stationGreen.name))) - ) - } - - #Preview("errGetDueTimes (offline)") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingDueTimes( - stationRedLongName, LuasStrings.errorNoInternet)) - ) - } -#endif diff --git a/LuasWatchiOS/Previews/PreviewsAppStartup.swift b/LuasWatchiOS/Previews/PreviewsAppStartup.swift deleted file mode 100644 index 06df194..0000000 --- a/LuasWatchiOS/Previews/PreviewsAppStartup.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// - -import LuasKit -import SwiftUI - -#if DEBUG - - let genericAuthError = "Some generic auth error" - - #Preview("locAuth unknown") { - LuasMainScreen() - .environmentObject( - makeAppModel(state: .locationAuthorizationUnknown)) - } - - #Preview("getloc") { - LuasMainScreen() - .environmentObject( - makeAppModel(state: .gettingLocation)) - } - - #Preview("err - locDisabled") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingLocation(LuasStrings.locationServicesDisabled))) - } - - #Preview("err - locDenied") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingLocation(LuasStrings.locationAccessDenied))) - } - - // swiftlint:disable:next line_length - let longGenericError = - "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." - - #Preview("err - locManErr") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingLocation(longGenericError))) - } - - #Preview("err - authErr") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingLocation( - LuasStrings.gettingLocationAuthError(genericAuthError)))) - } - - #Preview("err - other") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingLocation(LuasStrings.gettingLocationOtherError))) - } - - #Preview("err - far") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingLocation(LuasStrings.tooFarAway))) - } - - #Preview("err - far larger") { - LuasMainScreen() - .environmentObject( - makeAppModel( - state: .errorGettingLocation(LuasStrings.locationServicesDisabled)) - ) - .environment(\.sizeCategory, .accessibilityExtraExtraLarge) - } - -#endif diff --git a/LuasWatchiOS/Previews/PreviewsData.swift b/LuasWatchiOS/Previews/PreviewsData.swift new file mode 100644 index 0000000..8c94178 --- /dev/null +++ b/LuasWatchiOS/Previews/PreviewsData.swift @@ -0,0 +1,175 @@ +// +// Created by Roland Gropmair on 25/09/2021. +// Copyright © 2021 mApps.ie. All rights reserved. +// + +import CoreLocation +import LuasAPI +import LuasApp +import SwiftUI + +#if DEBUG + + let locationBluebell = CLLocation( + latitude: CLLocationDegrees(53.3292817872831), + longitude: CLLocationDegrees(-6.33382500275916)) + + let locationMarlborough = CLLocation( + latitude: CLLocationDegrees(53.3492448734525), + longitude: CLLocationDegrees(-6.25773158174389)) + + let userLocation = CLLocation( + latitude: locationBluebell.coordinate.latitude + 0.00425, + longitude: locationBluebell.coordinate.longitude + 0.005) + + private let stationRed = TrainStation( + stationIdShort: "LUAS8", + shortCode: "BLU", + route: .red, + name: "Bluebell", + location: locationBluebell) + + let stationRedLongName = TrainStation( + stationIdShort: "LUAS8", + shortCode: "BLU", + route: .red, + name: "Bluebell Luas Stop long name", + location: locationBluebell) + + private let trainRed1_outbound = Train( + destination: "LUAS The Point", direction: "Outbound", dueTime: "Due") + private let trainRed2_outbound = Train( + destination: "LUAS The Point", direction: "Outbound", dueTime: "2") + private let trainRed3_outbound = Train( + destination: "LUAS Connolly", direction: "Outbound", dueTime: "5") + private let trainRed4_outbound = Train( + destination: "LUAS The Point", direction: "Outbound", dueTime: "7") + private let trainRed5_outbound = Train( + destination: "LUAS The Point", direction: "Outbound", dueTime: "9") + private let trainRed6_outbound = Train( + destination: "LUAS Connolly", direction: "outbound", dueTime: "11") + private let trainRed7_outbound = Train( + destination: "LUAS Connolly", direction: "outbound", dueTime: "15") + + private let trainRed1_inbound = Train( + destination: "LUAS Tallaght", direction: "Inbound", dueTime: "Due") + private let trainRed2_inbound = Train( + destination: "LUAS Tallaght", direction: "Inbound", dueTime: "4") + private let trainRed3_inbound = Train( + destination: "LUAS Saggart", direction: "Inbound", dueTime: "5") + private let trainRed4_inbound = Train( + destination: "LUAS Tallaght", direction: "Inbound", dueTime: "7") + private let trainRed5_inbound = Train( + destination: "LUAS Saggart", direction: "Inbound", dueTime: "9") + private let trainRed6_inbound = Train( + destination: "LUAS Saggart", direction: "Inbound", dueTime: "12") + private let trainRed7_inbound = Train( + destination: "LUAS Saggart", direction: "Inbound", dueTime: "14") + + let trainsRed_1_1 = TrainsByDirection( + station: stationRed, + inbound: [trainRed3_inbound], + outbound: [trainRed2_outbound]) + let trainsRed_2_1 = TrainsByDirection( + station: stationRed, + inbound: [trainRed1_inbound, trainRed3_inbound], + outbound: [trainRed2_outbound]) + let trainsRed_3_2 = TrainsByDirection( + station: stationRedLongName, + inbound: [trainRed1_inbound, trainRed2_inbound, trainRed3_inbound], + outbound: [trainRed1_outbound, trainRed2_outbound]) + let trainsRed_4_4 = TrainsByDirection( + station: stationRed, + inbound: [trainRed1_inbound, trainRed2_inbound, trainRed3_inbound, trainRed4_inbound], + outbound: [trainRed1_outbound, trainRed2_outbound, trainRed3_outbound, trainRed4_outbound]) + let trainsRed_0_4 = TrainsByDirection( + station: stationRed, + inbound: [], + outbound: [trainRed1_outbound, trainRed2_outbound, trainRed3_outbound, trainRed4_outbound]) + let trainsRed_7_7 = TrainsByDirection( + station: stationRedLongName, + inbound: [ + trainRed1_inbound, trainRed2_inbound, trainRed3_inbound, trainRed4_inbound, trainRed5_inbound, + trainRed6_inbound, trainRed7_inbound, + ], + outbound: [ + trainRed1_outbound, trainRed2_outbound, trainRed3_outbound, trainRed4_outbound, + trainRed5_outbound, trainRed6_outbound, + trainRed7_outbound, + ]) + + let stationGreen = TrainStation( + stationIdShort: "LUAS69", + shortCode: "PHI", + route: .green, + name: "Phibsboro", + location: locationBluebell) + + private let trainGreen1 = Train( + destination: "LUAS Broombridge", direction: "Outbound", dueTime: "Due") + private let trainGreen2 = Train( + destination: "LUAS Broombridge", direction: "Outbound", dueTime: "9") + private let trainGreen3 = Train( + destination: "LUAS Sandyford", direction: "Inbound", dueTime: "12") + let trainsGreen = TrainsByDirection( + station: stationGreen, + inbound: [trainGreen3], + outbound: [trainGreen1, trainGreen2]) + + let noTrainsGreen = TrainsByDirection( + station: stationGreen, inbound: [], outbound: []) + + let stationOneWay = TrainStation( + stationIdShort: "LUAS62", + shortCode: "MAR", + route: .green, + name: "Marlborough", + location: locationMarlborough, + stationType: .oneway) + let trainsMarlborough = TrainsByDirection( + station: stationOneWay, + inbound: [trainGreen3], + outbound: []) + + let trainsOneWayStation = TrainsByDirection( + station: stationOneWay, + inbound: [trainGreen2, trainGreen3], + outbound: []) + + private let stationFinalStop = TrainStation( + stationIdShort: "stationIdShort", + shortCode: "TAL", + route: .red, + name: "Tallaght", + location: locationBluebell, + stationType: .terminal) + let trainsFinalStop = TrainsByDirection( + station: stationFinalStop, + inbound: [trainRed1_outbound, trainRed3_outbound], + outbound: []) + + let trainsNoTrains = TrainsByDirection(station: stationGreen, inbound: [], outbound: []) + + let trainsNoOutboundTrains = TrainsByDirection( + station: stationGreen, inbound: [trainGreen1], outbound: []) + + let lotsOfTrains = TrainsByDirection( + station: stationGreen, + inbound: [trainGreen1, trainGreen2, trainGreen3, trainGreen1, trainGreen2, trainGreen3], + outbound: [trainGreen1, trainGreen2, trainGreen3, trainGreen1, trainGreen2, trainGreen3]) + + let trainLongNameOne = TrainsByDirection( + station: stationGreen, + inbound: [ + Train(destination: "LUAS The Long Point Station", direction: "Outbound", dueTime: "Due") + ], outbound: []) + + let trainLongNameThree = TrainsByDirection( + station: stationGreen, + inbound: [ + Train(destination: "LUAS The Long Point Station", direction: "Outbound", dueTime: "Due"), + Train(destination: "LUAS The Long Point Station", direction: "Outbound", dueTime: "12"), + Train(destination: "LUAS The Long Point Station", direction: "Outbound", dueTime: "100"), + ], outbound: []) + +#endif diff --git a/LuasWatchiOS/Previews/PreviewsDataiOS.swift b/LuasWatchiOS/Previews/PreviewsDataiOS.swift deleted file mode 100644 index ca2e927..0000000 --- a/LuasWatchiOS/Previews/PreviewsDataiOS.swift +++ /dev/null @@ -1,82 +0,0 @@ -//// -//// Created by Roland Gropmair on 25/09/2021. -//// Copyright © 2021 mApps.ie. All rights reserved. -//// -// -//import SwiftUI -//import CoreLocation -// -//import LuasKit -// -//#if DEBUG -// -//// Bluebell station -//let locationBluebell = CLLocation(latitude: CLLocationDegrees(53.3292817872831), -// longitude: CLLocationDegrees(-6.33382500275916)) -// -//let userLocation = CLLocation(latitude: locationBluebell.coordinate.latitude + 0.00425, -// longitude: locationBluebell.coordinate.longitude + 0.005) -// -//let stationRed = TrainStation(stationId: "stationId", -// stationIdShort: "LUAS8", -// shortCode: "BLU", -// route: .red, -// name: "Bluebell Luas Stop", -// locationBluebell: locationBluebell) -//let trainRed1_outbound = Train(destination: "LUAS The Point", direction: "Outbound", dueTime: "Due") -//let trainRed2_outbound = Train(destination: "LUAS Tallaght", direction: "Outbound", dueTime: "9") -//let trainRed3_outbound = Train(destination: "LUAS Connolly", direction: "Inbound", dueTime: "12") -// -//let trainsRed_1_1 = TrainsByDirection(trainStation: stationRed, -// inbound: [trainRed3_outbound], -// outbound: [trainRed2_outbound]) -//let trainsRed_2_1 = TrainsByDirection(trainStation: stationRed, -// inbound: [trainRed1_outbound, trainRed3_outbound], -// outbound: [trainRed2_outbound]) -//let trainsRed_3_2 = TrainsByDirection(trainStation: stationRed, -// inbound: [trainRed1_outbound, trainRed2_outbound, trainRed3_outbound], -// outbound: [trainRed1_outbound, trainRed2_outbound]) -//let trainsRed_4_4 = TrainsByDirection(trainStation: stationRed, -// inbound: [trainRed1_outbound, trainRed2_outbound, trainRed3_outbound, trainRed3_outbound], -// outbound: [trainRed1_outbound, trainRed2_outbound, trainRed3_outbound, trainRed3_outbound]) -// -//let stationGreen = TrainStation(stationId: "stationId", -// stationIdShort: "LUAS69", -// shortCode: "PHI", -// route: .green, -// name: "Phibsboro", -// locationBluebell: locationBluebell) -// -//let trainGreen1 = Train(destination: "LUAS Broombridge", direction: "Outbound", dueTime: "Due") -//let trainGreen2 = Train(destination: "LUAS Broombridge", direction: "Outbound", dueTime: "9") -//let trainGreen3 = Train(destination: "LUAS Sandyford", direction: "Inbound", dueTime: "12") -// -//let trainsGreen = TrainsByDirection(trainStation: stationGreen, -// inbound: [trainGreen3], -// outbound: [trainGreen1, trainGreen2]) -// -//let stationOneWay = TrainStation(stationId: "stationId", -// stationIdShort: "LUAS69", -// shortCode: "MAR", -// route: .green, -// name: "Marlborough", -// locationBluebell: locationBluebell, -// stationType: .oneway) -//let trainsOneWayStation = TrainsByDirection(trainStation: stationOneWay, -// inbound: [trainGreen2, trainGreen3], -// outbound: []) -// -//let stationFinalStop = TrainStation(stationId: "stationId", -// stationIdShort: "stationIdShort", -// shortCode: "TAL", -// route: .red, -// name: "Tallaght", -// locationBluebell: locationBluebell, -// stationType: .terminal) -//let trainsFinalStop = TrainsByDirection(trainStation: stationFinalStop, -// inbound: [trainRed1_outbound, trainRed3_outbound], -// outbound: []) -//let directionBoth: Direction = .both -// -//#endif -// diff --git a/LuasWatchiOS/Previews/PreviewsHelpers.swift b/LuasWatchiOS/Previews/PreviewsHelpers.swift new file mode 100644 index 0000000..4d2643b --- /dev/null +++ b/LuasWatchiOS/Previews/PreviewsHelpers.swift @@ -0,0 +1,52 @@ +// +// Created by Roland Gropmair on 24/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +#if DEBUG + + @MainActor + func makeTabView(_ appModel: AppModel, _ route: Route) -> some View { + + @State var selectedStation: TrainStation? = trainsGreen.station + + return NavigationSplitView { + SidebarView(selectedStation: $selectedStation) + } detail: { + TabView(selection: $selectedStation) { + StationView() + .environmentObject(appModel) +// .containerBackground( +// route.color.gradient, +// for: .tabView) + } + } + .environmentObject(appModel) + .modelContainer(Previews().container) + } + + @MainActor + func luasMainScreen(state: AppState) -> some View { + let appModel = AppModel(state) + appModel.appMode = .favourite(stationGreen) + + return LuasMainScreen() + .environmentObject(appModel) + .modelContainer(Previews().container) + } + + func makeAppModel( + state: AppState, appMode: AppMode = .specific(stationGreen), locationDenied: Bool = false + ) -> AppModel { + let appModel = AppModel(state) + appModel.appMode = appMode + appModel.locationDenied = locationDenied + + return appModel + } + +#endif diff --git a/LuasWatchiOS/Previews/PreviewsLoadingStates.swift b/LuasWatchiOS/Previews/PreviewsLoadingStates.swift new file mode 100644 index 0000000..3c2c0f4 --- /dev/null +++ b/LuasWatchiOS/Previews/PreviewsLoadingStates.swift @@ -0,0 +1,42 @@ +// +// Created by Roland Gropmair on 14/05/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +#if DEBUG + + #Preview("loading") { + luasMainScreen( + state: .loadingDueTimes(stationGreen, + cachedTrains: nil) + ) + } + + #Preview("loading (cached)") { + luasMainScreen( + state: .loadingDueTimes(stationGreen, + cachedTrains: trainsGreen) + ) + } + + #Preview("loading 1Way (cached)") { + luasMainScreen( + state: .loadingDueTimes( + stationOneWay, + cachedTrains: trainsMarlborough + ) + ) + } + + #Preview("noTrains") { + luasMainScreen(state: .foundDueTimes(noTrainsGreen)) + } + + #Preview("OK") { + luasMainScreen(state: .foundDueTimes(trainsGreen)) + } +#endif diff --git a/LuasWatchiOS/Previews/PreviewsSidebar.swift b/LuasWatchiOS/Previews/PreviewsSidebar.swift new file mode 100644 index 0000000..f7c7755 --- /dev/null +++ b/LuasWatchiOS/Previews/PreviewsSidebar.swift @@ -0,0 +1,46 @@ +// +// Created by Roland Gropmair on 31/03/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +#if DEBUG + + #Preview("normal") { + @Previewable @State var selectedStation: TrainStation? + let appModel = makeAppModel( + state: AppState(.foundDueTimes(trainsOneWayStation)), + appMode: .favourite(stationGreen)) + + SidebarView(selectedStation: $selectedStation) + .environmentObject(appModel) + .modelContainer(Previews().container) + } + + #Preview("err far away") { + @Previewable @State var selectedStation: TrainStation? + let appModel = makeAppModel( + state: AppState( + .errorGettingStationTooFarAway(LuasStrings.tooFarAway)), + appMode: .closest) + + SidebarView(selectedStation: $selectedStation) + .environmentObject(appModel) + .modelContainer(Previews().container) + } + + #Preview("loc denied") { + @Previewable @State var selectedStation: TrainStation? + + let appModel = makeAppModel( + state: AppState(.foundDueTimes(trainsOneWayStation)), + appMode: .favourite(stationGreen), locationDenied: true) + + SidebarView(selectedStation: $selectedStation) + .environmentObject(appModel) + .modelContainer(Previews().container) + } +#endif diff --git a/LuasWatchiOS/Previews/PreviewsStates.swift b/LuasWatchiOS/Previews/PreviewsStates.swift new file mode 100644 index 0000000..acb504c --- /dev/null +++ b/LuasWatchiOS/Previews/PreviewsStates.swift @@ -0,0 +1,43 @@ +// +// Created by Roland Gropmair on 21/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +#if DEBUG + + #Preview("idle") { + luasMainScreen(state: .idle) + } + + #Preview("gettingLoc") { + luasMainScreen(state: .gettingLocation) + } + + #Preview("authUnk") { + luasMainScreen(state: .locationAuthorizationUnknown) + } + + #Preview("locErr") { + luasMainScreen(state: .errorGettingLocation("Error getting location.")) + } + + #Preview("errStation") { + luasMainScreen( + state: .errorGettingStationTooFarAway( + "Some internal error getting station.")) + } + + #Preview("errFarAway") { + luasMainScreen(state: .errorGettingStationTooFarAway(LuasStrings.tooFarAway)) + } + + #Preview("errLoading") { + luasMainScreen( + state: .errorGettingDueTimes( + stationGreen, "Error loading due times - could not access internet?")) + } +#endif diff --git a/LuasWatchiOS/Previews/PreviewsStationView.swift b/LuasWatchiOS/Previews/PreviewsStationView.swift new file mode 100644 index 0000000..eefc143 --- /dev/null +++ b/LuasWatchiOS/Previews/PreviewsStationView.swift @@ -0,0 +1,54 @@ +// +// Created by Roland Gropmair on 21/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +#if DEBUG +#Preview("Phibs (not fav; userLoc)") { + makeTabView( + AppModel( .foundDueTimes( trainsGreen ), + userLocation: locationBluebell), + .green + ) +} + +#Preview("Phibs (not fav)") { + makeTabView(AppModel(.foundDueTimes(trainsGreen)), .green) +} + +#Preview("1/1") { + makeTabView(AppModel(.foundDueTimes(trainsRed_1_1)), .red) +} + +#Preview("2/1") { + makeTabView(AppModel(.foundDueTimes(trainsRed_2_1)), .red) +} + +#Preview("3/2") { + makeTabView(AppModel(.foundDueTimes(trainsRed_3_2)), .red) +} + +#Preview("4/4") { + makeTabView(AppModel(.foundDueTimes(trainsRed_4_4)), .red) +} + +#Preview("0/4") { + makeTabView(AppModel(.foundDueTimes(trainsRed_0_4)), .red) +} + +#Preview("7/7") { + makeTabView(AppModel(.foundDueTimes(trainsRed_7_7)), .red) +} + +#Preview("OneWay") { + makeTabView(AppModel(.foundDueTimes(trainsOneWayStation)), .green) +} + +#Preview("Final") { + makeTabView(AppModel(.foundDueTimes(trainsFinalStop)), .red) +} +#endif diff --git a/LuasWatchiOS/Previews/PreviewsStationView_EdgeCases.swift b/LuasWatchiOS/Previews/PreviewsStationView_EdgeCases.swift new file mode 100644 index 0000000..4c3d56d --- /dev/null +++ b/LuasWatchiOS/Previews/PreviewsStationView_EdgeCases.swift @@ -0,0 +1,30 @@ +// +// Created by Roland Gropmair on 24/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +#if DEBUG + #Preview("No trains") { + makeTabView(AppModel(.foundDueTimes(trainsNoTrains)), .green) + } + + #Preview("No out") { + makeTabView(AppModel(.foundDueTimes(trainsNoOutboundTrains)), .green) + } + + #Preview("Lots") { + makeTabView(AppModel(.foundDueTimes(lotsOfTrains)), .green) + } + + #Preview("Long name 1") { + makeTabView(AppModel(.foundDueTimes(trainLongNameOne)), .green) + } + + #Preview("Long name 3") { + makeTabView(AppModel(.foundDueTimes(trainLongNameThree)), .green) + } +#endif diff --git a/LuasWatchiOS/Previews/PreviewsStationsModal.swift b/LuasWatchiOS/Previews/PreviewsStationsModal.swift deleted file mode 100644 index 903b672..0000000 --- a/LuasWatchiOS/Previews/PreviewsStationsModal.swift +++ /dev/null @@ -1,29 +0,0 @@ -//// -//// Created by Roland Gropmair on 25/09/2021. -//// Copyright © 2021 mApps.ie. All rights reserved. -//// -// -//import LuasKit -//import SwiftUI -// -//#if DEBUG -// // swiftlint:disable:next type_name -// struct Preview_StationsModal: PreviewProvider { -// @State static var isPresented: Bool = true -// -// static var previews: some View { -// -// Group { -// ChangeStationButton.StationsSelectionModal() -// .previewDisplayName("StationsSelectionModal") -// -// ChangeStationButton.StationsModal(stations: TrainStations.sharedFromFile.greenLineStations) -// {} -// .previewDisplayName("StationsModal = Green Line") -// -// ChangeStationButton.StationsModal(stations: TrainStations.sharedFromFile.redLineStations) {} -// .previewDisplayName("StationsModal = Red Line") -// } -// } -// } -//#endif diff --git a/LuasWatchiOS/Previews/PreviewsTapOverlay.swift b/LuasWatchiOS/Previews/PreviewsTapOverlay.swift deleted file mode 100644 index 4033ae4..0000000 --- a/LuasWatchiOS/Previews/PreviewsTapOverlay.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Created by Roland Gropmair on 10/05/2023. -// Copyright © 2023 mApps.ie. All rights reserved. -// - -import LuasKit -import SwiftUI - -#if DEBUG - #Preview("tapOverlay") { - ZStack { - LuasMainScreen() - .environmentObject( - makeAppModel(state: .foundDueTimes(trainsRed_1_1))) - -// LuasMainScreen() -// .overlayView("Showing outbound trains only") - } - } -#endif diff --git a/LuasWatchiOS/Views/ChangeStationButton.swift b/LuasWatchiOS/Views/ChangeStationButton.swift deleted file mode 100644 index 05bb14f..0000000 --- a/LuasWatchiOS/Views/ChangeStationButton.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// - -import LuasKit -import SwiftUI - -struct ChangeStationButton: View { - - @Binding var isStationsModalPresented: Bool - - var body: some View { - - VStack { - - Spacer(minLength: 30) - - Button("Select other station") { - isStationsModalPresented = true - } - .frame(maxHeight: 32) - .background(Color(red: 82 / 255, green: 53 / 255, blue: 214 / 255, opacity: 0.8)) - .cornerRadius(12) - } - .sheet( - isPresented: $isStationsModalPresented, - content: { - StationsSelectionModal() - }) - } - - struct StationsSelectionModal: View { - @Environment(\.dismiss) private var dismiss - @EnvironmentObject var appState: AppState - - var body: some View { - - NavigationView(content: { - - ScrollView { - NavigationLink(destination: greenStationsModal()) { - VStack { - Image(systemName: "arrow.up.arrow.down") - Text("Green Line Stations") - } - } - - NavigationLink(destination: redStationsModal()) { - VStack { - Image(systemName: "arrow.right.arrow.left") - Text("Red Line Stations") - } - } - - Button( - action: { - switch appState.state { - case .foundDueTimes(let trains, let location), - .updatingDueTimes(let trains, let location): - guard - let closest = TrainStations.sharedFromFile.closestStation( - from: location, route: trains.trainStation.route.other) - else { - assertionFailure("expected to find closest station from *other* line") - return - } - MyUserDefaults.saveSelectedStation(closest) - dismiss() - retriggerTimer() - - default: - assertionFailure("expected foundDueTimes here") - return - } - - }, - label: { - VStack { - Image(systemName: "location") - Text("Closest Other Line Station") - .font(.footnote) - } - }) - - if MyUserDefaults.userSelectedSpecificStation() != nil { - Button( - action: { - MyUserDefaults.wipeUserSelectedStation() - dismiss() - retriggerTimer() - }, - label: { - VStack { - Image(systemName: "location") - Text("Closest Station") - } - }) - } - } - }) - - } - - @ViewBuilder - private func greenStationsModal() -> some View { - StationsModal(stations: TrainStations.sharedFromFile.greenLineStations) { - dismiss() - } - } - - @ViewBuilder - private func redStationsModal() -> some View { - StationsModal(stations: TrainStations.sharedFromFile.redLineStations) { - dismiss() - } - } - } - - struct StationsModal: View { - @State var stations: [TrainStation] - - /// challenge here: we could use Environment(\.dismiss); but that only dismisses this Stations nav view, - /// so it's then back to StationsSelection modal. instead, we want to dismiss the *entire* flow - var dismissAllModal: () -> Void - - var body: some View { - // swiftlint:disable multiple_closures_with_trailing_closure - List { - ForEach(self.stations, id: \.stationId) { (station) in - // need a button here because just text only supports tap on the text but not full width - Button(action: { - myPrint("☣️ tap \(station) -> save") - - MyUserDefaults.saveSelectedStation(station) - - /// sometimes crash on watchOS 9 - /// [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior - DispatchQueue.main.async { - dismissAllModal() - /// start 12sec timer right now - /// this also has logic in there to immediately show this selected station if we have (quite current) current location - - retriggerTimer() - } - }) { - Text(station.name) - .font(.system(.headline)) - } - } - } - } - } -} -// swiftlint:enable multiple_closures_with_trailing_closure - -extension View { - - fileprivate func retriggerTimer() { - NotificationCenter.default.post( - Notification(name: Notification.Name("LuasWatch.RetriggerTimer"))) - } -} diff --git a/LuasWatchiOS/Views/Collection+Extensions.swift b/LuasWatchiOS/Views/Collection+Extensions.swift new file mode 100644 index 0000000..12b54b2 --- /dev/null +++ b/LuasWatchiOS/Views/Collection+Extensions.swift @@ -0,0 +1,14 @@ +// +// Created by Roland Gropmair on 03/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import Foundation + +// https://stackoverflow.com/questions/25329186/safe-bounds-checked-array-lookup-in-swift-through-optional-bindings +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/LuasWatchiOS/Views/DetailView/ClosestStationsView.swift b/LuasWatchiOS/Views/DetailView/ClosestStationsView.swift new file mode 100644 index 0000000..a9add4b --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/ClosestStationsView.swift @@ -0,0 +1,38 @@ +// +// Created by Roland Gropmair on 02/02/2025. +// Copyright © 2025 mApps.ie. All rights reserved. +// + +import CoreLocation +import LuasAPI +import LuasApp +import SwiftData +import SwiftUI + +struct ClosestStationsView: View { + var userLocation: CLLocation + + var body: some View { + VStack { + Text("Closest Stations") + .font(.headline) + + let fiveClosestStations = TrainStations() + .closestStationsSorted(from: userLocation) + .prefix(5) + + List(fiveClosestStations) { station in + Text(station.name) + } + } + } +} + +#Preview { + let closeLocation = CLLocation( + latitude: locationBluebell.coordinate.latitude + 0.00425, + longitude: locationBluebell.coordinate.longitude + 0.005 + ) + + ClosestStationsView(userLocation: closeLocation) +} diff --git a/LuasWatchiOS/Views/DetailView/DoubleTimetableView.swift b/LuasWatchiOS/Views/DetailView/DoubleTimetableView.swift new file mode 100644 index 0000000..a8c23d0 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/DoubleTimetableView.swift @@ -0,0 +1,59 @@ +// +// Created by Roland Gropmair on 01/03/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct DoubleTimetableView: View { + + @EnvironmentObject private var appModel: AppModel + + let trainsByDirection: TrainsByDirection +} + +extension DoubleTimetableView { + + var body: some View { + + VStack { + if trainsByDirection.inbound.count == 0 { + NoTrainsView() + + } else { + /// noOverflowSmall cuts off after 3 - WIP: show overflow in subsequent tabView + ForEach(trainsByDirection.inboundNoOverflowSmall, id: \.id) { + DueView( + destination: $0.destinationDescription, + due: $0.dueTimeDescriptionShort) + } + if trainsByDirection.inboundHasOverflowSmall { + OverflowDotsView() + } + Spacer() + } + + Divider() + + if trainsByDirection.outbound.count == 0 { + NoTrainsView() + + } else { + /// noOverflowSmall cuts off after 3 - WIP: show overflow in subsequent tabView + ForEach(trainsByDirection.outboundNoOverflowSmall, id: \.id) { + DueView( + destination: $0.destinationDescription, + due: $0.dueTimeDescriptionShort) + } + if trainsByDirection.outboundHasOverflowSmall { + OverflowDotsView() + } + Spacer() + } + } + .timeTableStyle() + .opacity(appModel.appState.isLoading ? 0.52 : 1.0) + } +} diff --git a/LuasWatchiOS/Views/DetailView/DueView.swift b/LuasWatchiOS/Views/DetailView/DueView.swift new file mode 100644 index 0000000..5e25e6a --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/DueView.swift @@ -0,0 +1,28 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import SwiftUI + +struct DueView: View { + let destination: String + let due: String +} + +extension DueView { + + var body: some View { + + HStack { + Text(destination) + .font(.title2) + .monospaced() + Spacer() + Text(due) + .font(.title2) + .monospaced() + } + .foregroundColor(.yellow) + } +} diff --git a/LuasWatchiOS/Views/GrantLocationAuthView.swift b/LuasWatchiOS/Views/DetailView/GrantLocationAuthView.swift similarity index 94% rename from LuasWatchiOS/Views/GrantLocationAuthView.swift rename to LuasWatchiOS/Views/DetailView/GrantLocationAuthView.swift index 0d79a59..31e9e39 100644 --- a/LuasWatchiOS/Views/GrantLocationAuthView.swift +++ b/LuasWatchiOS/Views/DetailView/GrantLocationAuthView.swift @@ -3,7 +3,8 @@ // Copyright © 2023 mApps.ie. All rights reserved. // -import LuasKit +import LuasAPI +import LuasApp import SwiftUI struct GrantLocationAuthView: View { diff --git a/LuasWatchiOS/Views/DetailView/LuasTextView.swift b/LuasWatchiOS/Views/DetailView/LuasTextView.swift new file mode 100644 index 0000000..4ecf9fb --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/LuasTextView.swift @@ -0,0 +1,25 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import SwiftUI + +struct LuasTextView: View { + var text: String + + var body: some View { + HStack { + Spacer() + Text(text) + .font(.title2) + .monospaced() + Spacer() + } + .foregroundColor(.yellow) + .timeTableStyle() + .toolbar { + ToolbarInactive() + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/NoTrainsView.swift b/LuasWatchiOS/Views/DetailView/NoTrainsView.swift new file mode 100644 index 0000000..0f9a911 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/NoTrainsView.swift @@ -0,0 +1,23 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct NoTrainsView: View { + + var body: some View { + VStack { + Spacer() + Text(LuasStrings.noTrains) + .font(.title2) + .monospaced() + .foregroundColor(.yellow) + .frame(maxWidth: .infinity) + Spacer() + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/OverflowDotsView.swift b/LuasWatchiOS/Views/DetailView/OverflowDotsView.swift new file mode 100644 index 0000000..20b0632 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/OverflowDotsView.swift @@ -0,0 +1,14 @@ +// +// Created by Roland Gropmair on 01/03/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import SwiftUI + +struct OverflowDotsView: View { + var body: some View { + Text("...") + .offset(CGSize(width: 0, height: -6)) + .frame(height: 1) + } +} diff --git a/LuasWatchiOS/Views/DetailView/SimpleTimetableView.swift b/LuasWatchiOS/Views/DetailView/SimpleTimetableView.swift new file mode 100644 index 0000000..2b3d2a0 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/SimpleTimetableView.swift @@ -0,0 +1,55 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct SimpleTimetableView: View { + + @EnvironmentObject private var appModel: AppModel + + let trainsByDirection: TrainsByDirection + let direction: Direction +} + +extension SimpleTimetableView { + + var body: some View { + + var trains = [Train]() + var hasOverflow = false + + switch direction { + /// noOverflowLarge cuts off after 6 - WIP: show overflow in subsequent tabView + case .inbound: + trains = trainsByDirection.inboundNoOverflowLarge + hasOverflow = trainsByDirection.inboundHasOverflowLarge + case .outbound: + trains = trainsByDirection.outboundNoOverflowLarge + hasOverflow = trainsByDirection.outboundHasOverflowLarge + case .both: + assertionFailure("expected either .inbound OR .outbound - not .both") + trains = trainsByDirection.inbound + } + + // we should always have train here, see where we're calling this view from + assert(trains.count > 0) + + return VStack { + ForEach(trains, id: \.id) { + DueView( + destination: $0.destinationDescription, + due: $0.dueTimeDescriptionShort) + } + if hasOverflow { + OverflowDotsView() + } + Spacer() + } + .timeTableStyle() + .opacity(appModel.appState.isLoading ? 0.52 : 1.0) + } +} diff --git a/LuasWatchiOS/Views/DetailView/StationTimesView.swift b/LuasWatchiOS/Views/DetailView/StationTimesView.swift new file mode 100644 index 0000000..d8580bf --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/StationTimesView.swift @@ -0,0 +1,123 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftData +import SwiftUI + +struct StationTimesView: View { + + @EnvironmentObject private var appModel: AppModel + @Environment(\.modelContext) private var modelContext + + @State private var direction: Direction = .both + + let trainStation: TrainStation + let trains: TrainsByDirection? +} + +extension StationTimesView { + + var body: some View { + + NavigationStack { + + VStack { + Text(trainStation.name) + .font(.largeTitle) + .padding(.bottom) + + if let trains { + + timetableView(for: trains) + + if let userLocation = appModel.latestLocation { + ClosestStationsView(userLocation: userLocation) + } + + } else { + + // no cachedTrains: we're loading that station for the first time + TrainsViewLoading() + .timeTableStyle() + } + Spacer() + } + + .onAppear { + direction = modelContext.directionConsideringStationType(for: trainStation.shortCode) + } + + .toolbar { + StationToolbar( + direction: $direction, + trainStation: trainStation) + } + } + } + + @ViewBuilder + fileprivate func timetableView(for trains: TrainsByDirection) -> some View { + + HStack { + Text("Destination") + .font(.title2) + Spacer() + Text("Minutes") + .font(.title2) + } + .padding(.horizontal, 26) + .padding(.bottom, -14) + + if trains.station.allowsSwitchingDirection { + + switch direction { + + case .inbound: + if trains.inbound.isEmpty { + NoTrainsView() + .timeTableStyle() + } else { + SimpleTimetableView( + trainsByDirection: trains, direction: .inbound) + } + + case .outbound: + if trains.outbound.isEmpty { + NoTrainsView() + .timeTableStyle() + } else { + SimpleTimetableView( + trainsByDirection: trains, direction: .outbound) + } + + case .both: + DoubleTimetableView( + trainsByDirection: trains) + } + + } else { + + // we have a .terminal or .oneway station -> only show the inbound or outbound trains + if !trains.inbound.isEmpty { + + SimpleTimetableView( + trainsByDirection: trains, direction: .inbound) + + } else if !trains.outbound.isEmpty { + + SimpleTimetableView( + trainsByDirection: trains, direction: .outbound) + + } else { + + NoTrainsView() + .timeTableStyle() + } + + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/StationToolbar.swift b/LuasWatchiOS/Views/DetailView/StationToolbar.swift new file mode 100644 index 0000000..51859e4 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/StationToolbar.swift @@ -0,0 +1,91 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct StationToolbar { + + @Environment(\.modelContext) private var modelContext + + @EnvironmentObject private var appModel: AppModel + + // have to let SwiftUI know that underlying context has changed - can we avoid the isFavourite state? + @State private var isFavourite: Bool = false + + @State private var isSwitchingDirectionEnabled: Bool = true + + @Binding var direction: Direction + + let trainStation: TrainStation +} + +extension StationToolbar: ToolbarContent { + + var body: some ToolbarContent { + + // for now: hide Map button + // ToolbarItem(placement: .topBarTrailing) { + // Button { + // // Perform an action here. + // } label: { + // Image(systemName: "map") + // } + // } + + ToolbarItemGroup(placement: .bottomBar) { + + /// Change direction + Button { + withAnimation { + direction = direction.next + } + + let shortCode = trainStation.shortCode + myPrint("\(#function) createOrUpdate \(shortCode) to \(direction)") + modelContext.createOrUpdate(shortCode: shortCode, to: direction) + + } label: { + + if trainStation.allowsSwitchingDirection { + + switch direction { + case .inbound: + Image(systemName: "arrow.left") + case .outbound: + Image(systemName: "arrow.right") + case .both: + Image(systemName: "arrow.left.arrow.right") + } + + } else { + // if station is terminal or a one way stop: show hard coded arrow; we disable the button below + Image(systemName: "arrow.right") + } + } + .onAppear { + isSwitchingDirectionEnabled = trainStation.allowsSwitchingDirection + } + .disabled(!isSwitchingDirectionEnabled) + + if appModel.appState.isLoading { + Text(LuasStrings.loadingDueTimes) + .font(.subheadline) + } + + /// Favourite + Button { + modelContext.toggleFavouriteStation(shortCode: trainStation.shortCode) + isFavourite.toggle() + } label: { + isFavourite ? Image(systemName: "heart.fill") : Image(systemName: "heart") + } + .onAppear { + isFavourite = modelContext.doesFavouriteStationExist(shortCode: trainStation.shortCode) + } + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/StationToolbarLoading.swift b/LuasWatchiOS/Views/DetailView/StationToolbarLoading.swift new file mode 100644 index 0000000..74c786f --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/StationToolbarLoading.swift @@ -0,0 +1,53 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct StationToolbarLoading { + + var isFavourite: Bool + var direction: Direction + var isSwitchingDirectionEnabled: Bool +} + +extension StationToolbarLoading: ToolbarContent { + + var body: some ToolbarContent { + + ToolbarItemGroup(placement: .bottomBar) { + + /// Change direction + Button { + // nop + } label: { + + if isSwitchingDirectionEnabled { + + switch direction { + case .inbound: + Image(systemName: "arrow.left") + case .outbound: + Image(systemName: "arrow.right") + case .both: + Image(systemName: "arrow.left.arrow.right") + } + + } else { + // if station is terminal or a one way stop: show hard coded arrow; we disable the button below + Image(systemName: "arrow.right") + } + } + .disabled(!isSwitchingDirectionEnabled) + + Text(LuasStrings.loadingDueTimes) + .font(.subheadline) + + /// Favourite + isFavourite ? Image(systemName: "heart.fill") : Image(systemName: "heart") + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/StationView.swift b/LuasWatchiOS/Views/DetailView/StationView.swift new file mode 100644 index 0000000..fc51709 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/StationView.swift @@ -0,0 +1,49 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct StationView { + + @EnvironmentObject private var appModel: AppModel +} + +extension StationView: View { + + var body: some View { + + switch appModel.appState { + + case .idle: + LuasTextView(text: "LuasWatch is starting...") + + case .gettingLocation: + LuasTextView(text: "Getting location...") + + case .locationAuthorizationUnknown: + // WIP we need new approach to trigger location prompt via appModel? + GrantLocationAuthView(didTapButton: { + appModel.appState = .gettingLocation + }) + + case .errorGettingLocation: + LuasTextView(text: appModel.appState.description) + + case .errorGettingStationTooFarAway(let errorMessage): + LuasTextView(text: errorMessage) + + case .loadingDueTimes(let trainStation, let cachedTrains): + StationTimesView(trainStation: trainStation, trains: cachedTrains) + + case .errorGettingDueTimes(_, let message): + LuasTextView(text: message) + + case .foundDueTimes(let trains): + StationTimesView(trainStation: trains.station, trains: trains) + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/ToolbarInactive.swift b/LuasWatchiOS/Views/DetailView/ToolbarInactive.swift new file mode 100644 index 0000000..2b8dfc3 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/ToolbarInactive.swift @@ -0,0 +1,37 @@ +// +// Created by Roland Gropmair on 10/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct ToolbarInactive { + // no properties +} + +extension ToolbarInactive: ToolbarContent { + + var body: some ToolbarContent { + + ToolbarItemGroup(placement: .bottomBar) { + + /// Change direction + Button { + // nop + } label: { + Image(systemName: "arrow.left.arrow.right") + } + .disabled(true) + + /// Favourite + Button { + // nop + } label: { + Image(systemName: "heart") + } + .disabled(true) + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/TrainsViewLoading.swift b/LuasWatchiOS/Views/DetailView/TrainsViewLoading.swift new file mode 100644 index 0000000..a14eb32 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/TrainsViewLoading.swift @@ -0,0 +1,22 @@ +// +// Created by Roland Gropmair on 25/05/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasApp +import SwiftUI + +struct TrainsViewLoading: View { + + var body: some View { + VStack { + Spacer() + Text(LuasStrings.trainsLoading) + .font(.body) + .monospaced() + .foregroundColor(.yellow) + .frame(maxWidth: .infinity) + Spacer() + } + } +} diff --git a/LuasWatchiOS/Views/DetailView/View+LuasFormatting.swift b/LuasWatchiOS/Views/DetailView/View+LuasFormatting.swift new file mode 100644 index 0000000..918a8f7 --- /dev/null +++ b/LuasWatchiOS/Views/DetailView/View+LuasFormatting.swift @@ -0,0 +1,18 @@ +// +// Created by Roland Gropmair on 24/02/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import SwiftUI + +extension View { + + func timeTableStyle() -> some View { + self + .frame(height: 200.0) + .padding(16) + .background(.black) + .border(.secondary).cornerRadius(2) + .padding(14) + } +} diff --git a/LuasWatchiOS/Views/HeaderView.swift b/LuasWatchiOS/Views/HeaderView.swift deleted file mode 100644 index 172424d..0000000 --- a/LuasWatchiOS/Views/HeaderView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// - -import LuasKit -import SwiftUI - -struct HeaderView: View { - var station: TrainStation - - @Binding var direction: Direction? - @Binding var overlayTextAfterTap: String? - - var body: some View { - ZStack { - - Image(station.route == .green ? "HeaderGreen" : "HeaderRed") - .resizable() - .frame(maxWidth: .infinity, minHeight: 36, maxHeight: 36, alignment: .trailing) - - HStack { - - Image( - systemName: imageName( - for: direction ?? .both, - allowsSwitchingDirection: self.station.allowsSwitchingDirection) - ) - .resizable() - .foregroundColor(.gray) - .frame(width: 25, height: 25) - .offset(x: 12) - - Spacer(minLength: 16) - - Text(station.name) - .lineLimit(1) - .font(.system(.headline)) - .foregroundColor(.black) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.trailing, 24) - - } - }.onTapGesture { - withAnimation { - handleTap(station) - } - } - } - - fileprivate func imageName( - for direction: Direction, - allowsSwitchingDirection: Bool - ) -> String { - if !allowsSwitchingDirection { - // let's not be specific (yet) whether this station is outbound or inbound only - return "arrow.right.circle.fill" - } - - switch direction { - case .both: - return "arrow.right.arrow.left.circle.fill" - case .inbound: - return "arrow.right.circle.fill" - case .outbound: - return "arrow.left.circle.fill" - } - } - - fileprivate func handleTap(_ trainStation: TrainStation) { - if trainStation.allowsSwitchingDirection { - direction = Direction.direction(for: trainStation.name).next() - Direction.setDirection(for: trainStation.name, to: direction!) - overlayTextAfterTap = text(for: direction!) - } else { - // we don't allow switching direction -> show toast as an explanation - overlayTextAfterTap = - trainStation.isFinalStop - ? LuasStrings.switchingDirectionsNotAllowedForFinalStop - : LuasStrings.switchingDirectionsNotAllowedForOnewayStop - } - } - - fileprivate func text(for direction: Direction) -> String { - switch direction { - case .both: - return "Showing both directions" - case .inbound: - return "Showing inbound trains only" - case .outbound: - return "Showing outbound trains only" - } - } -} diff --git a/LuasWatchiOS/Views/LoadingAnimationView.swift b/LuasWatchiOS/Views/LoadingAnimationView.swift deleted file mode 100644 index e3197ff..0000000 --- a/LuasWatchiOS/Views/LoadingAnimationView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// - -import LuasKit -import SwiftUI - -extension LuasView { - - @ViewBuilder - internal func loadingAnimationView() -> some View { - - VStack { - Text(self.appState.state.description) - .multilineTextAlignment(.center) - .padding() - - ZStack { - - Circle() - .stroke(Colors.luasPurple, lineWidth: 6) - .scaleEffect(isAnimating ? 2.8 : 1) - .animation(slowAnimation, value: isAnimating) - .blur(radius: 8) - - Circle() - .fill(Colors.luasGreen) - .scaleEffect(isAnimating ? 1.5 : 1) - .animation(standardAnimation, value: isAnimating) - - Circle() - .stroke(Colors.luasRed, lineWidth: 3) - .scaleEffect(isAnimating ? 2.0 : 1) - .animation(slowAnimation, value: isAnimating) - - }.frame(width: CGFloat(20), height: CGFloat(20)) - .onAppear { self.isAnimating = true } - .padding(25) - } - } - - fileprivate var standardAnimation: Animation { - Animation - .easeInOut(duration: 0.7) - .repeatForever() - } - - fileprivate var slowAnimation: Animation { - Animation - .easeInOut(duration: 1.4) - .repeatForever() - } -} diff --git a/LuasWatchiOS/Views/LuasMainScreen.swift b/LuasWatchiOS/Views/LuasMainScreen.swift new file mode 100644 index 0000000..0779374 --- /dev/null +++ b/LuasWatchiOS/Views/LuasMainScreen.swift @@ -0,0 +1,38 @@ +// +// Created by Roland Gropmair on 02/11/2023. +// Copyright © 2023 mApps.ie. All rights reserved. +// + +import SwiftUI + +import LuasAPI +import LuasApp + +struct LuasMainScreen { + + @EnvironmentObject var appModel: AppModel + @State var isMenuPresented: Bool = false + + private static let trainStations = TrainStations() +} + +extension LuasMainScreen: View { + + var body: some View { + + NavigationView { + + StationView() + .sheet(isPresented: $isMenuPresented) { + SidebarView(selectedStation: $appModel.selectedStation) + } + .toolbar { + ToolbarItemGroup(placement: .topBarLeading) { + Button("Menu") { + isMenuPresented = true + } + } + } + } + } +} diff --git a/LuasWatchiOS/Views/LuasView.swift b/LuasWatchiOS/Views/LuasView.swift deleted file mode 100644 index 9ab2a96..0000000 --- a/LuasWatchiOS/Views/LuasView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// Created by Roland Gropmair on 04/08/2019. -// Copyright © 2019 mApps.ie. All rights reserved. -// - -import Combine -import CoreLocation -import LuasKit -import SwiftUI - -struct LuasView: View { - - @EnvironmentObject var appState: AppState - - @State private var direction: Direction? - - @State internal var isAnimating = false - - @State internal var overlayTextAfterTap: String? - @State internal var overlayTextViewOpacity: Double = 1.0 - - var body: some View { - - Group { - switch appState.state { - - case .locationAuthorizationUnknown: - GrantLocationAuthView(didTapButton: { - appState.state = .gettingLocation - }) - - case .gettingLocation: - loadingAnimationView() - - case .errorGettingLocation: - Text(self.appState.state.description) - .multilineTextAlignment(.center) - .frame(idealHeight: .greatestFiniteMagnitude) - - case .errorGettingStation(let errorMessage): - Text(errorMessage) - .multilineTextAlignment(.center) - .frame(idealHeight: .greatestFiniteMagnitude) - - // we do get location here in this enum as well, but we ignore it in the UI - case .gettingDueTimes: - Text(self.appState.state.description) - .multilineTextAlignment(.center) - - // bit confusing: this enum has second parameter 'errorString', but it's not shown here - // because it's surfaced via the appState's `description` - case .errorGettingDueTimes(let trainStation, _): - - ScrollView { - VStack { - HeaderView( - station: trainStation, direction: $direction, - overlayTextAfterTap: $overlayTextAfterTap) - - Spacer(minLength: 20) - - Text(self.appState.state.description) - .multilineTextAlignment(.center) - - ChangeStationButton(isStationsModalPresented: $appState.isStationsModalPresented) - } - } - - case .foundDueTimes(let trains, let location): - foundDueTimesView(for: trains, location: location) - .transition(.opacity) - - case .updatingDueTimes(let trains, let location): - updatingDueTimesView(for: trains, location: location) - .transition(.opacity) - } - } - } - - fileprivate func foundDueTimesView(for trains: TrainsByDirection, location: CLLocation) - -> some View - { - ZStack { - VStack { - HeaderView( - station: trains.trainStation, direction: $direction, - overlayTextAfterTap: $overlayTextAfterTap) - - TrainsListView( - trains: trains, direction: direction ?? .both, - location: location) - - }.onAppear { - forceUpdateDirection(trainStationName: trains.trainStation.name) - } - - tapOverlayView() - } - } - - // not ideal: lots of repetition compared to above - @ViewBuilder - fileprivate func updatingDueTimesView(for trains: TrainsByDirection, location: CLLocation) - -> some View - { - VStack { - ZStack { - HeaderView( - station: trains.trainStation, direction: $direction, - overlayTextAfterTap: $overlayTextAfterTap) - Rectangle() - .foregroundColor(.black).opacity(0.7) - Text("Updating...") - .font(.system(.footnote)) - } - .frame(height: 36) // avoid jumping - - TrainsListView( - trains: trains, direction: direction ?? .both, - location: location) - - }.onAppear { - forceUpdateDirection(trainStationName: trains.trainStation.name) - } - } - - // challenge: if station changed since last time, it doesn't pick persisted one -> need to force update direction here to fix - fileprivate func forceUpdateDirection(trainStationName: String) { - if self.direction != Direction.direction(for: trainStationName) { - self.direction = Direction.direction(for: trainStationName) - myPrint("🟢 foundDueTimes -> updated direction \(String(describing: self.direction))") - } - } -} diff --git a/LuasWatchiOS/Views/Sidebar/AllStationsListView.swift b/LuasWatchiOS/Views/Sidebar/AllStationsListView.swift new file mode 100644 index 0000000..c249cdb --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/AllStationsListView.swift @@ -0,0 +1,79 @@ +// +// Created by Roland Gropmair on 03/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftData +import SwiftUI + +struct AllStationsListView { + @EnvironmentObject var appModel: AppModel + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + @State var stations: [TrainStation] + + private static let trainStations = TrainStations() +} + +extension AllStationsListView: View { + + var body: some View { + + NavigationView(content: { + + ScrollView { + NavigationLink( + destination: stationsListView( + stations: Self.trainStations.greenLineStations) + ) { + LineRow(route: .green, isHighlighted: false) + } + + NavigationLink( + destination: stationsListView( + stations: Self.trainStations.redLineStations) + ) { + LineRow(route: .red, isHighlighted: false) + } + } + }) + .navigationTitle("Add to favourites") + } + + @ViewBuilder + private func stationsListView(stations: [TrainStation]) -> some View { + StationsModal( + stations: stations, + highlightedStation: appModel.highlightedStation, + action: { station in + + if modelContext.doesFavouriteStationExist(shortCode: station.shortCode) + == false + { + modelContext.insert(FavouriteStation(shortCode: station.shortCode)) + } else { + myPrint("Favourite station already exists -> ignore") + } + + DispatchQueue.main.async { + dismiss() + } + } + ) + .navigationTitle("Add to favourites") + } +} + +#if DEBUG + + #Preview("All Stations (green)") { + + AllStationsListView(stations: TrainStations().greenLineStations) + .environmentObject(makeAppModel(state: .gettingLocation)) + .modelContainer(Previews().container) + } + +#endif diff --git a/LuasWatchiOS/Views/Sidebar/FavouritesHeaderView.swift b/LuasWatchiOS/Views/Sidebar/FavouritesHeaderView.swift new file mode 100644 index 0000000..649e1ee --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/FavouritesHeaderView.swift @@ -0,0 +1,51 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct FavouritesHeaderView { + + @Environment(\.modelContext) private var modelContext + + @State var isStationsModalPresented = false + + private static let trainStations = TrainStations() +} + +extension FavouritesHeaderView: View { + + var body: some View { + HStack { + Text("Favourites") + .font(.subheadline) + .frame(minHeight: 40) + Spacer() + Button( + action: { + isStationsModalPresented = true + }, + label: { + Image(systemName: "plus.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 30) + } + ) + .buttonStyle(.borderless) + .sheet( + isPresented: $isStationsModalPresented, + content: { + AllStationsListView(stations: Self.trainStations.greenLineStations) + }) + } + } +} + +#Preview("HeaderView") { + FavouritesHeaderView() + .modelContainer(Previews().container) +} diff --git a/LuasWatchiOS/Views/Sidebar/FavouritesSidebarView.swift b/LuasWatchiOS/Views/Sidebar/FavouritesSidebarView.swift new file mode 100644 index 0000000..ccf9787 --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/FavouritesSidebarView.swift @@ -0,0 +1,106 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftData +import SwiftUI + +struct FavouritesSidebarView { + + @EnvironmentObject var appModel: AppModel + @Environment(\.dismiss) var dismiss + @Environment(\.modelContext) private var modelContext + + @Query(sort: \FavouriteStation.dateAdded, order: .reverse) + + private var favouriteStations: [FavouriteStation] + private static let trainStations = TrainStations() +} + +extension FavouritesSidebarView: View { + + var body: some View { + + if !favouriteStations.isEmpty { + + ForEach(favouriteStations) { station in + + let station = + Self.trainStations.station(shortCode: station.shortCode) + ?? TrainStation.unknown + + StationRow( + station: station, + isHighlighted: isHighlighted(for: station.name), + action: { + appModel.appMode = .favourite(station) + dismiss() + }) + + }.onDelete(perform: { indexSet in + + if let index = indexSet.first, + let item = favouriteStations[safe: index] + { + modelContext.delete(item) + } + }) + + } else { + + VStack { + Text("No favourite stations yet") + .font(.title3) + .padding() + .foregroundColor(.primary) + Text("Add one by tapping the plus button") + .font(.caption) + .multilineTextAlignment(.center) + .foregroundColor(.gray) + } + + } + } + + private func isHighlighted(for station: String) -> Bool { + if case .favourite(let favStation) = appModel.appMode, + favStation.name == station + { + return true + } else { + return false + } + } +} + +#if DEBUG + + #Preview("Favourites") { + + List { + Section { + FavouritesSidebarView() + } header: { + FavouritesHeaderView() + } + .modelContainer(Previews().container) + .environmentObject( + makeAppModel(state: .foundDueTimes(trainsOneWayStation))) + } + } + + #Preview("Favourites (empty)") { + + List { + Section { + FavouritesSidebarView() + } header: { + FavouritesHeaderView() + } + .modelContainer(Previews(addSample: false).container) + } + } +#endif diff --git a/LuasWatchiOS/Views/Sidebar/LineRow.swift b/LuasWatchiOS/Views/Sidebar/LineRow.swift new file mode 100644 index 0000000..17c90e1 --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/LineRow.swift @@ -0,0 +1,61 @@ +// +// Created by Roland Gropmair on 06/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftData +import SwiftUI + +struct LineRow: View { + + let route: Route + let isHighlighted: Bool +} + +extension LineRow { + + var body: some View { + HStack { + Text(route.text) + .fontWeight(isHighlighted ? .bold : .regular) + + Spacer() + route.image + .padding(5) + .background(route.color) + .cornerRadius(6) + } + } +} + +extension Route { + + var image: Image { + switch self { + case .red: + return Image(systemName: "arrow.up.arrow.down") + case .green: + return Image(systemName: "arrow.right.arrow.left") + } + } + + var text: String { + switch self { + case .red: + return "Red line stations" + case .green: + return "Green line stations" + } + } + + var color: Color { + switch self { + case .red: + return Color("luasRed") + case .green: + return Color("luasGreen") + } + } +} diff --git a/LuasWatchiOS/Views/Sidebar/LinesView.swift b/LuasWatchiOS/Views/Sidebar/LinesView.swift new file mode 100644 index 0000000..0ad1d17 --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/LinesView.swift @@ -0,0 +1,71 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct LinesView { + @EnvironmentObject var appModel: AppModel + + var actionGreen: () -> Void + var actionRed: () -> Void +} + +extension LinesView: View { + + var body: some View { + Button { + actionGreen() + } label: { + // so we center the lineRowView + HStack { + Spacer() + LineRow(route: .green, isHighlighted: isHighlighted(for: .green)) + Spacer() + } + } + .padding(.vertical, 8) + + Button { + actionRed() + } label: { + HStack { + Spacer() + LineRow(route: .red, isHighlighted: isHighlighted(for: .red)) + Spacer() + } + } + .padding(.vertical, 8) + } + + private func isHighlighted(for route: Route) -> Bool { + if case .specific(let specificStation) = appModel.appMode, + specificStation.route == route + { + return true + } else { + return false + } + } +} + +#if DEBUG + + #Preview("Lines") { + + List { + Section { + LinesView(actionGreen: {}, actionRed: {}) + .environmentObject(makeAppModel(state: .foundDueTimes(trainsGreen))) + } header: { + Text("Lines") + .font(.subheadline) + .frame(minHeight: 40) + } + } + } + +#endif diff --git a/LuasWatchiOS/Views/Sidebar/NearbyStationsView.swift b/LuasWatchiOS/Views/Sidebar/NearbyStationsView.swift new file mode 100644 index 0000000..6b20d00 --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/NearbyStationsView.swift @@ -0,0 +1,75 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct NearbyStationsView { + + @EnvironmentObject var appModel: AppModel + @Environment(\.dismiss) var dismiss +} + +extension NearbyStationsView: View { + + var body: some View { + + Button( + action: { + appModel.appMode = .closest + dismiss() + }, + label: { + HStack { + Text("Closest station") + Spacer() + Image(systemName: "location.fill.viewfinder") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 24) + } + .fontWeight(appModel.appMode == .closest ? .bold : .regular) + } + ).disabled(appModel.locationDenied == true) + + Button( + action: { + appModel.appMode = .closestOtherLine + dismiss() + }, + label: { + HStack { + Text("Closest other line station") + Spacer() + Image(systemName: "location.viewfinder") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 24) + } + .fontWeight(appModel.appMode == .closestOtherLine ? .bold : .regular) + + } + ).disabled(appModel.locationDenied == true) + } +} + +#if DEBUG + + #Preview("Nearby") { + + List { + Section { + NearbyStationsView() + .environmentObject( + makeAppModel(state: .foundDueTimes(trainsOneWayStation))) + } header: { + Text("Nearby") + .font(.subheadline) + .frame(minHeight: 40) + } + } + } +#endif diff --git a/LuasWatchiOS/Views/Sidebar/SidebarView.swift b/LuasWatchiOS/Views/Sidebar/SidebarView.swift new file mode 100644 index 0000000..7b1a06a --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/SidebarView.swift @@ -0,0 +1,106 @@ +// +// Created by Roland Gropmair on 01/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct SidebarView { + + @EnvironmentObject var appModel: AppModel + + @Binding var selectedStation: TrainStation? + + @State var isGreenStationsViewPresented = false + @State var isRedStationsViewPresented = false + + private static let trainStations = TrainStations() +} + +extension SidebarView: View { + + var body: some View { + + List(selection: $selectedStation) { + + /// Favourite Stations + Section { + FavouritesSidebarView() + } header: { + FavouritesHeaderView() + } + + /// Nearby + Section { + NearbyStationsView() + } header: { + Text("Nearby") + .font(.subheadline) + .frame(minHeight: 40) + } footer: { + + if appModel.locationDenied { + Text(LuasStrings.locationDeniedFooter) + } + + if case .errorGettingStationTooFarAway = appModel.appState { + Text(LuasStrings.tooFarAway) + } + } + + /// Lines Green / Red + Section { + LinesView( + actionGreen: { + isGreenStationsViewPresented = true + }, + actionRed: { + isRedStationsViewPresented = true + }) + } header: { + Text("Lines") + .font(.subheadline) + .frame(minHeight: 40) + } footer: { + + let shortVersion = + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + ?? "(unknown)" + let buildNumber = + Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" + + Text("\nApp Version \(shortVersion) (\(buildNumber))") + } + + } + .sheet( + isPresented: $isGreenStationsViewPresented, + content: { + StationsModal( + stations: Self.trainStations.greenLineStations, + highlightedStation: appModel.highlightedStation, + action: { + appModel.appMode = .specific($0) + isGreenStationsViewPresented = false + }) + } + ) + .sheet( + isPresented: $isRedStationsViewPresented, + content: { + StationsModal( + stations: Self.trainStations.redLineStations, + highlightedStation: appModel.highlightedStation, + action: { + appModel.appMode = .specific($0) + isRedStationsViewPresented = false + }) + } + ).containerBackground( + Color("luasTheme").gradient, + for: .navigation + ) + } +} diff --git a/LuasWatchiOS/Views/Sidebar/StationRow.swift b/LuasWatchiOS/Views/Sidebar/StationRow.swift new file mode 100644 index 0000000..ef52555 --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/StationRow.swift @@ -0,0 +1,40 @@ +// +// Created by Roland Gropmair on 06/01/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftData +import SwiftUI + +struct StationRow: View { + + var station: TrainStation + var isHighlighted: Bool + var action: () -> Void +} + +extension StationRow { + + var body: some View { + + Button( + action: { action() }, + label: { + + let dotColour = station.route == .red ? Color("luasRed") : Color("luasGreen") + + HStack { + Text(station.name) + .fontWeight(isHighlighted ? .bold : .regular) + Spacer() + Rectangle() + .cornerRadius(3) + .frame(width: 30, height: 20) + .foregroundColor(dotColour) + } + } + ) + } +} diff --git a/LuasWatchiOS/Views/Sidebar/StationsModal.swift b/LuasWatchiOS/Views/Sidebar/StationsModal.swift new file mode 100644 index 0000000..d3eef49 --- /dev/null +++ b/LuasWatchiOS/Views/Sidebar/StationsModal.swift @@ -0,0 +1,63 @@ +// +// Created by Roland Gropmair on 02/03/2024. +// Copyright © 2024 mApps.ie. All rights reserved. +// + +import LuasAPI +import LuasApp +import SwiftUI + +struct StationsModal: View { + + var stations: [TrainStation] + var highlightedStation: TrainStation? + + var action: (TrainStation) -> Void +} + +extension StationsModal { + + var body: some View { + ScrollViewReader { (reader) in + List { + ForEach(stations, id: \.stationIdShort) { (station) in + + // need a button here because just text only supports tap on the text but not full width + Button( + action: { + action(station) + }, + label: { + Text(station.name) + .font(.system(.headline)) + .fontWeight( + highlightedStation?.name == station.name ? .bold : .regular) + } + ) + .id(station.shortCode) + } + } + .onAppear { + // if we have a highlightedStation, scroll to it (will show up near bottom of screen) + guard let highlightedStation else { + return + } + reader.scrollTo(highlightedStation.shortCode) + } + } + } +} + +#if DEBUG + #Preview("Stations Modal (green)") { + + // highlight in preview doesn't work?? does it used StoredAppMode? + StationsModal( + stations: TrainStations().greenLineStations, + action: { _ in + // + } + ) + .environmentObject(makeAppModel(state: .foundDueTimes(trainsGreen))) + } +#endif diff --git a/LuasWatchiOS/Views/TapOverlayView.swift b/LuasWatchiOS/Views/TapOverlayView.swift deleted file mode 100644 index 6cab651..0000000 --- a/LuasWatchiOS/Views/TapOverlayView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// - -import SwiftUI - -extension LuasView { - - @ViewBuilder - internal func tapOverlayView() -> some View { - - if let text = overlayTextAfterTap { - overlayView(text) - } - } - - @ViewBuilder - internal func overlayView(_ text: String) -> some View { - ZStack { - Rectangle() - .foregroundColor(.black).opacity(0.82) - VStack { - Text(text) - .font(.body) - .padding() - .multilineTextAlignment(.center) - } - } - .offset(y: 40) - .frame(minHeight: 20, maxHeight: 240) - .opacity(overlayTextViewOpacity) - .onAppear { - withAnimation(Animation.easeOut.delay(1.5)) { - overlayTextViewOpacity = 0.0 - } - - // a bit ugly: reset so we're ready to show if user taps again - DispatchQueue.main.asyncAfter(deadline: .now() + 2.2) { - overlayTextAfterTap = nil - overlayTextViewOpacity = 1.0 - } - } - } -} diff --git a/LuasWatchiOS/Views/TrainsListView.swift b/LuasWatchiOS/Views/TrainsListView.swift deleted file mode 100644 index 13b7728..0000000 --- a/LuasWatchiOS/Views/TrainsListView.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// Created by Roland Gropmair on 25/09/2021. -// Copyright © 2021 mApps.ie. All rights reserved. -// - -import CoreLocation -import LuasKit -import SwiftUI - -struct TrainsListView: View { - let trains: TrainsByDirection - let direction: Direction - let location: CLLocation // current user location - - @EnvironmentObject var appState: AppState - - var distance: String? { - guard let distance = trains.trainStation.distance(from: location) else { return nil } - - return LuasStrings.distance(station: trains.trainStation, distance: distance) - } - - var body: some View { - - if trains.trainStation.isFinalStop || trains.trainStation.isOneWayStop { - // find the trains, either inbound or outbound - - if trains.inbound.count > 0 { - oneWayTrainsView(trains.inbound, distanceFooter: distance) - - } else if trains.outbound.count > 0 { - oneWayTrainsView(trains.outbound, distanceFooter: distance) - } else { - - // cannot use assert here?? - // assert(false, "we should have found either trains in inbound or outbound direction") - - VStack { - - VStack { - Spacer(minLength: 10) - Text("No Trains found") - Spacer(minLength: 10) - } - - ChangeStationButton(isStationsModalPresented: $appState.isStationsModalPresented) - } - } - - } else { - - // train station allows switching direction, so let's find out what the user has chosen - switch direction { - - case .both: - twoWayTrainsView(distanceFooter: distance) - - case .inbound: - oneWayTrainsView(trains.inbound, distanceFooter: distance) - - case .outbound: - oneWayTrainsView(trains.outbound, distanceFooter: distance) - } - } - } - - @ViewBuilder - private func oneWayTrainsView(_ trainsList: [Train], distanceFooter: String?) -> some View { - List { - Section(footer: ChangeStationButtonWithDistance(distanceFooter: distanceFooter)) { - - ForEach(trainsList, id: \.id) { - Text($0.dueTimeDescription) - } - } - } - } - - @ViewBuilder - private func twoWayTrainsView(distanceFooter: String?) -> some View { - List { - Section { - ForEach(self.trains.inbound, id: \.id) { - Text($0.dueTimeDescription) - } - } - - Section(footer: ChangeStationButtonWithDistance(distanceFooter: distanceFooter)) { - - ForEach(self.trains.outbound, id: \.id) { - Text($0.dueTimeDescription) - } - } - } - } - - @ViewBuilder - fileprivate func ChangeStationButtonWithDistance(distanceFooter: String?) -> some View { - VStack { - ChangeStationButton(isStationsModalPresented: $appState.isStationsModalPresented) - - if let distanceFooter { - Spacer(minLength: 12) - Text(distanceFooter) - .font(.footnote) - } - } - } -} diff --git a/README.md b/README.md index c887fb6..8d3f1c1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Unit Tests](https://github.com/roland9/LuasWatch/workflows/Unit%20Tests/badge.svg) +![Unit Tests](https://github.com/roland9/LuasWatch/actions/workflows/ios.yml/badge.svg) # LuasWatch Fast and Simple Luas for Apple Watch - Find the closest Stop & show DueTime diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6d48b9c..197ddf4 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -13,7 +13,7 @@ platform :ios do def build_app gym( - scheme: "LuasWatch WatchKit App", + scheme: "LuasWatch", destination: "generic/platform=watchOS", export_xcargs: "-allowProvisioningUpdates", # output_directory: "build/",