From ac80a2493658c90dc521f6132c39e57745e0343a Mon Sep 17 00:00:00 2001 From: Piv <18462828+Piv200@users.noreply.github.com> Date: Sun, 3 May 2020 19:33:27 +0930 Subject: [PATCH 1/3] Start fleshing out simple controller --- .../CarController/SimpleControllerView.swift | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/CarControlleriOS/CarController/SimpleControllerView.swift b/CarControlleriOS/CarController/SimpleControllerView.swift index a394d1f..da685a3 100644 --- a/CarControlleriOS/CarController/SimpleControllerView.swift +++ b/CarControlleriOS/CarController/SimpleControllerView.swift @@ -10,14 +10,25 @@ import SwiftUI struct SimpleControllerView: View { @EnvironmentObject var server: ServerData - @State var throttle: Float = 0 - @State var steering: Float = 0 + @State var throttle: Float = 0.5 + @State var steering: Float = 0.5 + var body: some View { HStack{ - Text("Opened Simple Controller!") - Slider(value: $throttle) - Slider(value: $steering) + Slider(value: $throttle, in: 0...1){_ in + self.throttle = 0.5} + .rotationEffect(.degrees(270)) + + Slider(value: $steering, in: 0...1){ + _ in self.steering = 0.5 + } + } + .onAppear(){ + // Start the gRPC updater. Should be in a separate class/struct to be easier to implement for SLAM Controller after. + } + .onDisappear(){ + } } } From 9376bd70edb2f7caee844ec7368898413bc34939 Mon Sep 17 00:00:00 2001 From: Piv <18462828+Piv200@users.noreply.github.com> Date: Mon, 4 May 2020 22:40:22 +0930 Subject: [PATCH 2/3] Simple Controller UI/gRPC working --- .../CarController.xcodeproj/project.pbxproj | 22 +++-- CarControlleriOS/CarController/PiLoader.swift | 76 ++++++++++++++++++ .../CarController/ServerData.swift | 3 +- .../CarController/Settings.bundle/Root.plist | 61 ++++++++++++++ .../Settings.bundle/en.lproj/Root.strings | Bin 0 -> 546 bytes .../CarController/SimpleControllerView.swift | 22 ++--- CarControlleriOS/build.gradle | 2 +- 7 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 CarControlleriOS/CarController/PiLoader.swift create mode 100644 CarControlleriOS/CarController/Settings.bundle/Root.plist create mode 100644 CarControlleriOS/CarController/Settings.bundle/en.lproj/Root.strings diff --git a/CarControlleriOS/CarController.xcodeproj/project.pbxproj b/CarControlleriOS/CarController.xcodeproj/project.pbxproj index ab9af95..bfa7185 100644 --- a/CarControlleriOS/CarController.xcodeproj/project.pbxproj +++ b/CarControlleriOS/CarController.xcodeproj/project.pbxproj @@ -21,7 +21,9 @@ 5A9EB280240100970053D3CF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5A9EB27F240100970053D3CF /* Assets.xcassets */; }; 5A9EB283240100970053D3CF /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5A9EB282240100970053D3CF /* Preview Assets.xcassets */; }; 5A9EB286240100970053D3CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5A9EB284240100970053D3CF /* LaunchScreen.storyboard */; }; - 5AE45131245CFF2800D82BAF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5AE45130245CFF2800D82BAF /* SwiftPackageProductDependency */; }; + 5AC1297B245EF22700CC19C3 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5AC1297A245EF22700CC19C3 /* Settings.bundle */; }; + 5AC1297D245FB95000CC19C3 /* PiLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC1297C245FB95000CC19C3 /* PiLoader.swift */; }; + 5AE45131245CFF2800D82BAF /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = 5AE45130245CFF2800D82BAF /* GRPC */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -41,6 +43,8 @@ 5A9EB282240100970053D3CF /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 5A9EB285240100970053D3CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 5A9EB287240100970053D3CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5AC1297A245EF22700CC19C3 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 5AC1297C245FB95000CC19C3 /* PiLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiLoader.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -48,7 +52,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5AE45131245CFF2800D82BAF /* BuildFile in Frameworks */, + 5AE45131245CFF2800D82BAF /* GRPC in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -122,6 +126,8 @@ 5A9EB281240100970053D3CF /* Preview Content */, 5A9C27122443F52500DBDF12 /* SimpleControllerView.swift */, 5A9C27142443F5B500DBDF12 /* ServerData.swift */, + 5AC1297A245EF22700CC19C3 /* Settings.bundle */, + 5AC1297C245FB95000CC19C3 /* PiLoader.swift */, ); path = CarController; sourceTree = ""; @@ -151,7 +157,7 @@ ); name = CarController; packageProductDependencies = ( - 5AE45130245CFF2800D82BAF /* SwiftPackageProductDependency */, + 5AE45130245CFF2800D82BAF /* GRPC */, ); productName = CarController; productReference = 5A9EB276240100960053D3CF /* CarController.app */; @@ -182,7 +188,7 @@ ); mainGroup = 5A9EB26D240100950053D3CF; packageReferences = ( - 5AE4512F245CFF2800D82BAF /* RemoteSwiftPackageReference */, + 5AE4512F245CFF2800D82BAF /* XCRemoteSwiftPackageReference "grpc-swift" */, ); productRefGroup = 5A9EB277240100960053D3CF /* Products */; projectDirPath = ""; @@ -198,6 +204,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5AC1297B245EF22700CC19C3 /* Settings.bundle in Resources */, 5A9EB286240100970053D3CF /* LaunchScreen.storyboard in Resources */, 5A9EB283240100970053D3CF /* Preview Assets.xcassets in Resources */, 5A9EB280240100970053D3CF /* Assets.xcassets in Resources */, @@ -216,6 +223,7 @@ 5A6B34552459AFCE0000E6FC /* SlamController.grpc.swift in Sources */, 5A6B34562459AFCE0000E6FC /* SlamController.pb.swift in Sources */, 5A6B34592459AFCE0000E6FC /* motorService.grpc.swift in Sources */, + 5AC1297D245FB95000CC19C3 /* PiLoader.swift in Sources */, 5A9EB27A240100960053D3CF /* AppDelegate.swift in Sources */, 5A6B345A2459AFCE0000E6FC /* motorService.pb.swift in Sources */, 5A6B34582459AFCE0000E6FC /* lidar_tracker.grpc.swift in Sources */, @@ -417,7 +425,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 5AE4512F245CFF2800D82BAF /* RemoteSwiftPackageReference */ = { + 5AE4512F245CFF2800D82BAF /* XCRemoteSwiftPackageReference "grpc-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/grpc/grpc-swift.git"; requirement = { @@ -428,9 +436,9 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 5AE45130245CFF2800D82BAF /* SwiftPackageProductDependency */ = { + 5AE45130245CFF2800D82BAF /* GRPC */ = { isa = XCSwiftPackageProductDependency; - package = 5AE4512F245CFF2800D82BAF /* RemoteSwiftPackageReference */; + package = 5AE4512F245CFF2800D82BAF /* XCRemoteSwiftPackageReference "grpc-swift" */; productName = GRPC; }; /* End XCSwiftPackageProductDependency section */ diff --git a/CarControlleriOS/CarController/PiLoader.swift b/CarControlleriOS/CarController/PiLoader.swift new file mode 100644 index 0000000..3e007dd --- /dev/null +++ b/CarControlleriOS/CarController/PiLoader.swift @@ -0,0 +1,76 @@ +// +// PiLoader.swift +// CarController +// +// Created by Michael Pivato on 4/5/20. +// Copyright © 2020 Michael Pivato. All rights reserved. +// + +import Foundation +import SwiftUI +import GRPC +import NIO + +class PiLoader: ObservableObject { + // Find a cleaner way to handle these properties + @Published var throttle: Float = 0.5 + @Published var steering: Float = 0.5 + let port: Int = 50051 + var stopped = false + + func makeClient(port: Int, group: EventLoopGroup) -> MotorControl_CarControlClient { + let channel = ClientConnection.insecure(group: group) + // TODO: Pass the host in based on the settings. + .connect(host: "10.0.0.55", port: port) + + return MotorControl_CarControlClient(channel: channel) + } + + func startUpdating() { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try? group.syncShutdownGracefully() + } + let client = makeClient(port: self.port, group: group) + let options = CallOptions(timeout: .seconds(rounding: 10)) + let call = client.stream_vehicle_2d(callOptions: options) + + call.response.whenFailure { error in + print("RecordRoute Failed: \(error)") + self.stop() + } + + call.status.whenComplete { _ in + print("Finished RecordRoute") + self.stop() + } + + call.response.whenSuccess { summary in + print("Finished") + self.stop() + } + + DispatchQueue.global(qos: .userInteractive).async{ + // Running in background. Do the update thread stuff. + while (!self.stopped){ + call.sendMessage(self.createProto(), promise: nil) + Thread.sleep(forTimeInterval: 0.2) + } + } + } + + func stop(){ + stopped = true + } + + func createProto() -> MotorControl_Vehicle2DRequest { + return .with{ + $0.throttle = .with{ + $0.throttle = self.throttle + } + $0.steering = .with{ + $0.steering = self.steering + } + } + } +} diff --git a/CarControlleriOS/CarController/ServerData.swift b/CarControlleriOS/CarController/ServerData.swift index 816144a..e23830e 100644 --- a/CarControlleriOS/CarController/ServerData.swift +++ b/CarControlleriOS/CarController/ServerData.swift @@ -10,8 +10,9 @@ import Foundation final class ServerData: ObservableObject{ - // TODO: Find a way to save/represent this stuff in iOS settings (user can access via settings app). + // TODO: Find a way to save/represent this stuff in iOS settings/preferences (user can access via settings app). // Then load the below values ar runtime, from settings. + // Ideally want to be able to open this settings page from within the app as well. @Published var port: Int = 50051 diff --git a/CarControlleriOS/CarController/Settings.bundle/Root.plist b/CarControlleriOS/CarController/Settings.bundle/Root.plist new file mode 100644 index 0000000..b1b6fea --- /dev/null +++ b/CarControlleriOS/CarController/Settings.bundle/Root.plist @@ -0,0 +1,61 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSGroupSpecifier + Title + Group + + + Type + PSTextFieldSpecifier + Title + Name + Key + name_preference + DefaultValue + + IsSecure + + KeyboardType + Alphabet + AutocapitalizationType + None + AutocorrectionType + No + + + Type + PSToggleSwitchSpecifier + Title + Enabled + Key + enabled_preference + DefaultValue + + + + Type + PSSliderSpecifier + Key + slider_preference + DefaultValue + 0.5 + MinimumValue + 0 + MaximumValue + 1 + MinimumValueImage + + MaximumValueImage + + + + + diff --git a/CarControlleriOS/CarController/Settings.bundle/en.lproj/Root.strings b/CarControlleriOS/CarController/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 0000000000000000000000000000000000000000..8cd87b9d6b20c1fbf87bd4db3db267fca5ad4df9 GIT binary patch literal 546 zcmaixOHRW;5JYRuDMndFh#Ua1V1d}N;sVAV2TO?uC3a9aJn*VxFrY}tnon0(S66#J z-d9>G>6W!ur(SDqlp`9nn~*(m%iWnv?yq`Qfp6XbK1?+om~~#r)ZnhkYQU_VbfjuT zHNn`CX<0sd*m1A}>&5sU$akD=GTXJ1e literal 0 HcmV?d00001 diff --git a/CarControlleriOS/CarController/SimpleControllerView.swift b/CarControlleriOS/CarController/SimpleControllerView.swift index da685a3..ee5188f 100644 --- a/CarControlleriOS/CarController/SimpleControllerView.swift +++ b/CarControlleriOS/CarController/SimpleControllerView.swift @@ -10,25 +10,27 @@ import SwiftUI struct SimpleControllerView: View { @EnvironmentObject var server: ServerData - @State var throttle: Float = 0.5 - @State var steering: Float = 0.5 - + + // Need lazy so that we can initialise with local properties. + @ObservedObject var grpcController: PiLoader = PiLoader() var body: some View { HStack{ - Slider(value: $throttle, in: 0...1){_ in - self.throttle = 0.5} + Slider(value: $grpcController.throttle, in: 0...1){_ in + self.grpcController.throttle = 0.5 + } .rotationEffect(.degrees(270)) - - Slider(value: $steering, in: 0...1){ - _ in self.steering = 0.5 + + Slider(value: $grpcController.steering, in: 0...1){_ in + self.grpcController.steering = 0.5 } } .onAppear(){ - // Start the gRPC updater. Should be in a separate class/struct to be easier to implement for SLAM Controller after. + self.grpcController.startUpdating() } .onDisappear(){ - + // Stop the gRPC updater. + self.grpcController.stop() } } } diff --git a/CarControlleriOS/build.gradle b/CarControlleriOS/build.gradle index ea6f5da..039b69b 100644 --- a/CarControlleriOS/build.gradle +++ b/CarControlleriOS/build.gradle @@ -12,5 +12,5 @@ dependencies { task copySwiftCode(type: Copy, dependsOn: configurations.swift) { // Copy python protobuf code from proto project. from zipTree(configurations.swift.asPath) - into './CarController/CarController' + into './CarController' } \ No newline at end of file From 4b3b960d222ac82033a1891e3a2a6b959d38e175 Mon Sep 17 00:00:00 2001 From: Piv <18462828+Piv200@users.noreply.github.com> Date: Wed, 6 May 2020 21:44:42 +0930 Subject: [PATCH 3/3] iOS simple controller works --- .vscode/launch.json | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../CarController/ContentView.swift | 14 +++- CarControlleriOS/CarController/PiLoader.swift | 67 +++++++++---------- .../CarController/ServerData.swift | 18 +++-- .../CarController/Settings.bundle/Root.plist | 42 +++--------- .../CarController/SimpleControllerView.swift | 38 ++++++----- 7 files changed, 90 insertions(+), 97 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ffe4adf..7c79804 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,9 +10,9 @@ "request": "launch", "module": "car", "env": { - "CAR_LIDAR": "LIDAR_RPLIDAR", + "CAR_LIDAR": "LIDAR_MOCK", "CAR_VEHICLE": "CAR_MOCK", - "LIDAR_DEVICE": "/dev/tty.usbserial-0001" + // "LIDAR_DEVICE": "/dev/tty.usbserial-0001" } }, { diff --git a/CarControlleriOS/CarController.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CarControlleriOS/CarController.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c90f41d..12fe0a3 100644 --- a/CarControlleriOS/CarController.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CarControlleriOS/CarController.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "e876fb37410e0036b98b5361bb18e6854739572b", - "version": "2.16.0" + "revision": "40bdad80882d307abe2c0bb36cf3bd4d3e03fe04", + "version": "2.16.1" } }, { diff --git a/CarControlleriOS/CarController/ContentView.swift b/CarControlleriOS/CarController/ContentView.swift index 5f8de90..7044469 100644 --- a/CarControlleriOS/CarController/ContentView.swift +++ b/CarControlleriOS/CarController/ContentView.swift @@ -15,16 +15,26 @@ struct ContentView: View { NavigationLink(destination: SimpleControllerView()){ Text("Simple Controller") } - // TODO: Change these when other functionality is implemented + NavigationLink(destination: SimpleControllerView()){ + Text("Gamepad controller") + } NavigationLink(destination: SimpleControllerView()){ Text("SLAM Controller") } NavigationLink(destination: SimpleControllerView()){ - Text("Tracking Controller") + Text("Lidar Tracking Controller") + } + NavigationLink(destination: SimpleControllerView()){ + Text("Mono Camera Tracking Controller") + } + NavigationLink(destination: SimpleControllerView()){ + Text("Hybrid Lidar Camera Tracking Controller") } } + .navigationBarTitle(Text("Controllers")) } + .navigationViewStyle(StackNavigationViewStyle()) } } diff --git a/CarControlleriOS/CarController/PiLoader.swift b/CarControlleriOS/CarController/PiLoader.swift index 3e007dd..1c42a58 100644 --- a/CarControlleriOS/CarController/PiLoader.swift +++ b/CarControlleriOS/CarController/PiLoader.swift @@ -15,42 +15,32 @@ class PiLoader: ObservableObject { // Find a cleaner way to handle these properties @Published var throttle: Float = 0.5 @Published var steering: Float = 0.5 - let port: Int = 50051 var stopped = false - func makeClient(port: Int, group: EventLoopGroup) -> MotorControl_CarControlClient { - let channel = ClientConnection.insecure(group: group) - // TODO: Pass the host in based on the settings. - .connect(host: "10.0.0.55", port: port) - - return MotorControl_CarControlClient(channel: channel) - } - - func startUpdating() { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try? group.syncShutdownGracefully() - } - let client = makeClient(port: self.port, group: group) - let options = CallOptions(timeout: .seconds(rounding: 10)) - let call = client.stream_vehicle_2d(callOptions: options) - - call.response.whenFailure { error in - print("RecordRoute Failed: \(error)") - self.stop() - } - - call.status.whenComplete { _ in - print("Finished RecordRoute") - self.stop() - } - - call.response.whenSuccess { summary in - print("Finished") - self.stop() - } - - DispatchQueue.global(qos: .userInteractive).async{ + func startUpdating(forPort port: Int, atHost host: String) { + DispatchQueue.global(qos: .background).async{ + let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) + defer { + try? group.syncShutdownGracefully() + } + let client = makeClient(port: port, host: host, group: group) + let options = CallOptions(timeout: .seconds(rounding: 10)) + let call = client.stream_vehicle_2d(callOptions: options) + + call.response.whenFailure { error in + print("Failed: \(error)") + self.stop() + } + + call.status.whenComplete { _ in + print("Finished") + self.stop() + } + + call.response.whenSuccess { summary in + print("Finished") + self.stop() + } // Running in background. Do the update thread stuff. while (!self.stopped){ call.sendMessage(self.createProto(), promise: nil) @@ -74,3 +64,12 @@ class PiLoader: ObservableObject { } } } + +func makeClient(port: Int, host: String, group: EventLoopGroup) -> MotorControl_CarControlClient { + let channel = ClientConnection.insecure(group: group) + // TODO: Pass the host in based on the settings. + .connect(host: host, port: port) + + + return MotorControl_CarControlClient(channel: channel) +} diff --git a/CarControlleriOS/CarController/ServerData.swift b/CarControlleriOS/CarController/ServerData.swift index e23830e..c049235 100644 --- a/CarControlleriOS/CarController/ServerData.swift +++ b/CarControlleriOS/CarController/ServerData.swift @@ -8,20 +8,18 @@ import Foundation +struct UserKeys{ + static let host = "host" + static let port = "port" +} + final class ServerData: ObservableObject{ - // TODO: Find a way to save/represent this stuff in iOS settings/preferences (user can access via settings app). - // Then load the below values ar runtime, from settings. - // Ideally want to be able to open this settings page from within the app as well. - - - @Published var port: Int = 50051 - @Published var grpcPort: Int = 50050 - @Published var host: String = "10.0.0.53" + @Published var host: String = UserDefaults.standard.string(forKey: UserKeys.host) ?? "10.0.0.53" + @Published var grpcPort: Int = UserDefaults.standard.integer(forKey: UserKeys.port) func load(){ - // Load the server values from settings, if they had been - // previously saved. + // Load the server values from settings, if they had been previously saved. } func save(){ diff --git a/CarControlleriOS/CarController/Settings.bundle/Root.plist b/CarControlleriOS/CarController/Settings.bundle/Root.plist index b1b6fea..1261bd8 100644 --- a/CarControlleriOS/CarController/Settings.bundle/Root.plist +++ b/CarControlleriOS/CarController/Settings.bundle/Root.plist @@ -6,23 +6,15 @@ Root PreferenceSpecifiers - - Type - PSGroupSpecifier - Title - Group - Type PSTextFieldSpecifier Title - Name + Host Key - name_preference + host DefaultValue - - IsSecure - + 10.0.0.53 KeyboardType Alphabet AutocapitalizationType @@ -32,29 +24,17 @@ Type - PSToggleSwitchSpecifier + PSTextFieldSpecifier Title - Enabled + Port Key - enabled_preference + port DefaultValue - - - - Type - PSSliderSpecifier - Key - slider_preference - DefaultValue - 0.5 - MinimumValue - 0 - MaximumValue - 1 - MinimumValueImage - - MaximumValueImage - + 50051 + IsSecure + + KeyboardType + NumberPad diff --git a/CarControlleriOS/CarController/SimpleControllerView.swift b/CarControlleriOS/CarController/SimpleControllerView.swift index ee5188f..fa866ec 100644 --- a/CarControlleriOS/CarController/SimpleControllerView.swift +++ b/CarControlleriOS/CarController/SimpleControllerView.swift @@ -10,27 +10,33 @@ import SwiftUI struct SimpleControllerView: View { @EnvironmentObject var server: ServerData - - // Need lazy so that we can initialise with local properties. @ObservedObject var grpcController: PiLoader = PiLoader() var body: some View { - HStack{ - Slider(value: $grpcController.throttle, in: 0...1){_ in - self.grpcController.throttle = 0.5 + VStack (alignment: .trailing, spacing: 0){ + Spacer() + HStack{ + // Move this up a bit, due to being rotated. + Slider(value: self.$grpcController.throttle, in: 0...1){_ in + self.grpcController.throttle = 0.5 + } + .offset(x: 200) + .frame(width: 300) + .rotationEffect(.degrees(270)) + Spacer() + Slider(value: self.$grpcController.steering, in: 0...1){_ in + self.grpcController.steering = 0.5 + } + .frame(width:300) + .offset(x: -50, y: -200) + .padding() } - .rotationEffect(.degrees(270)) - - Slider(value: $grpcController.steering, in: 0...1){_ in - self.grpcController.steering = 0.5 + .onAppear(){ + self.grpcController.startUpdating(forPort: self.server.grpcPort, atHost: self.server.host) + } + .onDisappear(){ + self.grpcController.stop() } - } - .onAppear(){ - self.grpcController.startUpdating() - } - .onDisappear(){ - // Stop the gRPC updater. - self.grpcController.stop() } } }