From 4d9bb45807cb858d1d1f5d1be09a0d160da9b9c7 Mon Sep 17 00:00:00 2001 From: Nick Bourdakos Date: Sat, 1 Apr 2023 15:13:29 -0400 Subject: [PATCH 1/4] Add example SwiftUI app for Nearby Connections --- .gitignore | 45 ++ connections/.DS_Store | Bin 0 -> 6148 bytes .../swift/NearbyConnections/Example/.DS_Store | Bin 0 -> 6148 bytes .../iOS Example.xcodeproj/project.pbxproj | 426 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 25 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../iOS Example/Assets.xcassets/Contents.json | 6 + .../Example/iOS Example/ConnectedView.swift | 57 +++ .../iOS Example/ConnectionRequestView.swift | 55 +++ .../Example/iOS Example/ContentView.swift | 84 ++++ .../iOS Example/DiscoveredEndpointView.swift | 47 ++ .../Example/iOS Example/EndpointDetail.swift | 55 +++ .../iOS Example/HelloConnectionsApp.swift | 29 ++ .../Example/iOS Example/Model/Config.swift | 37 ++ .../iOS Example/Model/ConnectedEndpoint.swift | 24 + .../iOS Example/Model/ConnectionRequest.swift | 25 + .../Model/DiscoveredEndpoint.swift | 23 + .../Example/iOS Example/Model/Model.swift | 251 +++++++++++ .../Example/iOS Example/Model/Payload.swift | 33 ++ .../Example/iOS Example/PayloadView.swift | 121 +++++ .../Preview Assets.xcassets/Contents.json | 6 + 24 files changed, 1388 insertions(+) create mode 100644 connections/.DS_Store create mode 100644 connections/swift/NearbyConnections/Example/.DS_Store create mode 100644 connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.pbxproj create mode 100644 connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/Contents.json create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/ConnectedView.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/ConnectionRequestView.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/ContentView.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/DiscoveredEndpointView.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/EndpointDetail.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/HelloConnectionsApp.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Model/Config.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectedEndpoint.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectionRequest.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Model/DiscoveredEndpoint.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Model/Model.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Model/Payload.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/PayloadView.swift create mode 100644 connections/swift/NearbyConnections/Example/iOS Example/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/.gitignore b/.gitignore index efb916b2..48464321 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,47 @@ build/** .build + +# We might want to create a real project config at some point, but for now we +# want to ignore adding the auto-generated one. +.vscode/ + +# Xcode + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +.build/ +.swiftpm/ + +# Carthage +Carthage/Build +.UlyssesRoot +.Ulysses-Settings.plist +.Ulysses-Group.plist diff --git a/connections/.DS_Store b/connections/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0ca2b44eb803e7db8d406f9abc63aa67ea3180eb GIT binary patch literal 6148 zcmeHKJBk895UtV?1QWvz`wHHm5k0|Nz(96ckP-Z*6M3#Z+P?ZE!(gVtF0Y{abywHx zre82MB_g`M?pGp95t+jc5r~noCR|;seX*X*;DQoNKaaL;!di80h5~3oFMTo)me- Y=Gd=^eW243cRG+i1Evd&3cOo^U%xdJ=Kufz literal 0 HcmV?d00001 diff --git a/connections/swift/NearbyConnections/Example/.DS_Store b/connections/swift/NearbyConnections/Example/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f5391216b8cfb231acaac848f4f08ec6720e05ad GIT binary patch literal 6148 zcmeHK!AiqG5Pe&Fh#wWM-Fr3z;1NVe(jxff0a=DmXZ#`9$QqXio;_84^QUV~Qu-phks%abA|SH`vh}t@8S$N1J1y|V}LbVBs)@c>kK#p&cKEN`9DOeU~bqcre6n5 zY5|Dyj7-9@^b(R24RgazkzXi5Q;C`yti=eLPJgtx+^|#BbOdWYg6$lvLkVejt{+o4 zLayl68E^)68931CNb3Le>;8W?$!E@hGw`Pv2*YAlOn4-#t-ZrZt&P+ + + + + diff --git a/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..677a6086 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "abseil", + "repositoryURL": "https://github.com/bourdakos1/abseil-cpp-SwiftPM.git", + "state": { + "branch": "cxx17", + "revision": "a7042563d167160f56e614cd1e2a32616510d9ec", + "version": null + } + }, + { + "package": "BoringSSL-GRPC", + "repositoryURL": "https://github.com/firebase/boringssl-SwiftPM.git", + "state": { + "branch": null, + "revision": "734a8247442fde37df4364c21f6a0085b6a36728", + "version": "0.7.2" + } + } + ] + }, + "version": 1 +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json b/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/Contents.json b/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/ConnectedView.swift b/connections/swift/NearbyConnections/Example/iOS Example/ConnectedView.swift new file mode 100644 index 00000000..57dfa6f0 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/ConnectedView.swift @@ -0,0 +1,57 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct ConnectedView: View { + var connection: ConnectedEndpoint + var onSendBytes : () -> () + var onDisconnect : () -> () + + var body: some View { + Section("Connection Actions") { + Button("Send Bytes") { + onSendBytes() + } + Button("Disconnect", role: .destructive) { + onDisconnect() + } + } + + if !connection.payloads.isEmpty { + Section(header: Text("Payloads")) { + ForEach(connection.payloads) { payload in + PayloadView(payload: payload) + } + } + } + } +} + +struct ConnectedView_Previews: PreviewProvider { + static var previews: some View { + Form { + ConnectedView( + connection: ConnectedEndpoint( + id: "", + endpointName: "Example" + ), + onSendBytes: {}, + onDisconnect: {} + ) + } + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/ConnectionRequestView.swift b/connections/swift/NearbyConnections/Example/iOS Example/ConnectionRequestView.swift new file mode 100644 index 00000000..9f5f59a9 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/ConnectionRequestView.swift @@ -0,0 +1,55 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct ConnectionRequestView: View { + var connectionRequest: ConnectionRequest + var onAcceptConnection : () -> () + var onRejectConnection : () -> () + + @State private var hasResponded = false + + var body: some View { + Section(footer: Text("Security code: \(connectionRequest.pin)")) { + Button("Accept") { + hasResponded = true + onAcceptConnection() + }.disabled(hasResponded) + Button("Reject", role: .destructive) { + hasResponded = true + onRejectConnection() + }.disabled(hasResponded) + } + } +} + +struct ConnectionRequestView_Previews: PreviewProvider { + static var previews: some View { + Form { + ConnectionRequestView( + connectionRequest: ConnectionRequest( + id: "", + endpointName: "Example", + pin: "1234", + shouldAccept: { _ in } + ), + onAcceptConnection: {}, + onRejectConnection: {} + ) + } + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/ContentView.swift b/connections/swift/NearbyConnections/Example/iOS Example/ContentView.swift new file mode 100644 index 00000000..651df340 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/ContentView.swift @@ -0,0 +1,84 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import NearbyConnections + +struct ContentView: View { + @EnvironmentObject private var model: Model + + let strategies: [Strategy: String] = [ + .cluster: "Cluster", + .pointToPoint: "Point to Point", + .star: "Star" + ] + + var body: some View { + NavigationSplitView { + Form { + TextField("Endpoint Name", text: $model.endpointName) + + Picker("Strategy", selection: $model.strategy) { + ForEach(Array(strategies), id: \.key) { key, value in + Text(value).tag(key) + } + } + + Toggle("Advertising", isOn: $model.isAdvertisingEnabled) + + Toggle("Discovery", isOn: $model.isDiscoveryEnabled) + + if !model.requests.isEmpty { + Section(header: Text("Pending Connections")) { + ForEach(model.requests) { request in + NavigationLink(value: request.id) { + Text(request.endpointName) + } + } + } + } + + if !model.connections.isEmpty { + Section(header: Text("Connections")) { + ForEach(model.connections) { connection in + NavigationLink(value: connection.id) { + Text(connection.endpointName) + } + } + } + } + + if !model.endpoints.isEmpty { + Section(header: Text("Endpoints")) { + ForEach(model.endpoints) { endpoint in + NavigationLink(value: endpoint.id) { + Text(endpoint.endpointName) + } + } + } + } + } + .navigationDestination(for: String.self) { endpointID in + EndpointDetail(endpointID: endpointID) + } + .navigationTitle("Hello Connections") + } detail: { + Text("No Endpoints Selected") + .font(.headline) + .foregroundColor(.secondary) + } + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/DiscoveredEndpointView.swift b/connections/swift/NearbyConnections/Example/iOS Example/DiscoveredEndpointView.swift new file mode 100644 index 00000000..c446e1ab --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/DiscoveredEndpointView.swift @@ -0,0 +1,47 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DiscoveredEndpointView: View { + var endpoint: DiscoveredEndpoint + var onRequestConnection : () -> () + + @State private var hasResponded = false + + var body: some View { + Section { + Button("Connect") { + hasResponded = true + onRequestConnection() + }.disabled(hasResponded) + } + } +} + +struct DiscoveredEndpointView_Previews: PreviewProvider { + static var previews: some View { + Form { + DiscoveredEndpointView( + endpoint: DiscoveredEndpoint( + id: "", + endpointName: "Example" + ), + onRequestConnection: {} + ) + } + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/EndpointDetail.swift b/connections/swift/NearbyConnections/Example/iOS Example/EndpointDetail.swift new file mode 100644 index 00000000..7875082f --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/EndpointDetail.swift @@ -0,0 +1,55 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct EndpointDetail: View { + var endpointID: String + + @EnvironmentObject private var model: Model + + var body: some View { + let endpoint = model.endpoints.first { $0.id == endpointID } + let connectionRequest = model.requests.first { $0.id == endpointID } + let connection = model.connections.first { $0.id == endpointID } + let name = connectionRequest?.endpointName ?? connection?.endpointName ?? endpoint?.endpointName + + Form { + if let endpoint { + DiscoveredEndpointView(endpoint: endpoint, onRequestConnection: { + model.requestConnection(to: endpointID) + }) + } + + if let connectionRequest { + ConnectionRequestView(connectionRequest: connectionRequest, onAcceptConnection: { + connectionRequest.shouldAccept(true) + }, onRejectConnection: { + connectionRequest.shouldAccept(false) + }) + } + + if let connection { + ConnectedView(connection: connection, onSendBytes: { + model.sendBytes(to: [endpointID]) + }, onDisconnect: { + model.disconnect(from: endpointID) + }) + } + } + .navigationTitle(name ?? "") + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/HelloConnectionsApp.swift b/connections/swift/NearbyConnections/Example/iOS Example/HelloConnectionsApp.swift new file mode 100644 index 00000000..5778c0e9 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/HelloConnectionsApp.swift @@ -0,0 +1,29 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@main +struct HelloConnectionsApp: App { + @StateObject private var model = Model() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(model) + } + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Model/Config.swift b/connections/swift/NearbyConnections/Example/iOS Example/Model/Config.swift new file mode 100644 index 00000000..64165f8b --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Model/Config.swift @@ -0,0 +1,37 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +#if os(iOS) || os(watchOS) || os(tvOS) +import UIKit +#endif +import NearbyConnections + +class Config { + static let serviceId = "com.google.location.nearby.apps.helloconnections" + static let defaultStategy = Strategy.cluster + static let defaultAdvertisingState = false + static let defaultDiscoveryState = false + static let bytePayload = "hello world" + +#if os(iOS) || os(watchOS) || os(tvOS) + static let defaultEndpointName = UIDevice.current.name +#elseif os(macOS) + static let defaultEndpointName = Host.current().localizedName ?? "Unknown macOS Device" +#else + static let defaultEndpointName = "Unknown Device" +#endif +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectedEndpoint.swift b/connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectedEndpoint.swift new file mode 100644 index 00000000..be5bb5f9 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectedEndpoint.swift @@ -0,0 +1,24 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NearbyConnections + +struct ConnectedEndpoint: Identifiable { + let id: EndpointID + let endpointName: String + var payloads: [Payload] = [] +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectionRequest.swift b/connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectionRequest.swift new file mode 100644 index 00000000..9b584c18 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Model/ConnectionRequest.swift @@ -0,0 +1,25 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NearbyConnections + +struct ConnectionRequest: Identifiable { + let id: EndpointID + let endpointName: String + let pin: String + let shouldAccept: ((Bool) -> Void) +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Model/DiscoveredEndpoint.swift b/connections/swift/NearbyConnections/Example/iOS Example/Model/DiscoveredEndpoint.swift new file mode 100644 index 00000000..ece0ab72 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Model/DiscoveredEndpoint.swift @@ -0,0 +1,23 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NearbyConnections + +struct DiscoveredEndpoint: Identifiable { + let id: EndpointID + let endpointName: String +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Model/Model.swift b/connections/swift/NearbyConnections/Example/iOS Example/Model/Model.swift new file mode 100644 index 00000000..8c4a10b1 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Model/Model.swift @@ -0,0 +1,251 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NearbyConnections + +class Model: ObservableObject { + @Published var endpointName = Config.defaultEndpointName { + didSet { + invalidateAdvertising() + } + } + @Published var strategy = Config.defaultStategy { + didSet { + invalidateAdvertising() + invalidateDiscovery() + } + } + @Published var isAdvertisingEnabled = Config.defaultAdvertisingState { + didSet { + invalidateAdvertising() + } + } + @Published var isDiscoveryEnabled = Config.defaultDiscoveryState { + didSet { + invalidateDiscovery() + } + } + + @Published private(set) var requests: [ConnectionRequest] = [] + @Published private(set) var connections: [ConnectedEndpoint] = [] + @Published private(set) var endpoints: [DiscoveredEndpoint] = [] + + var connectionManager: ConnectionManager! + var advertiser: Advertiser? + var discoverer: Discoverer? + + init() { + invalidateAdvertising() + invalidateDiscovery() + } + + private var isAdvertising = Config.defaultAdvertisingState + private func invalidateAdvertising() { + defer { + isAdvertising = isAdvertisingEnabled + } + if isAdvertising { + advertiser?.stopAdvertising() + } + if !isAdvertisingEnabled { + return + } + connectionManager = ConnectionManager(serviceID: Config.serviceId, strategy: strategy) + connectionManager.delegate = self + + advertiser = Advertiser(connectionManager: connectionManager) + advertiser?.delegate = self + advertiser?.startAdvertising(using: endpointName.data(using: .utf8)!) + } + + private var isDiscovering = Config.defaultDiscoveryState + private func invalidateDiscovery() { + defer { + isDiscovering = isDiscoveryEnabled + } + if isDiscovering { + discoverer?.stopDiscovery() + } + if !isDiscoveryEnabled { + return + } + connectionManager = ConnectionManager(serviceID: Config.serviceId, strategy: strategy) + connectionManager.delegate = self + + discoverer = Discoverer(connectionManager: connectionManager) + discoverer?.delegate = self + discoverer?.startDiscovery() + } + + func requestConnection(to endpointID: EndpointID) { + discoverer?.requestConnection(to: endpointID, using: endpointName.data(using: .utf8)!) + } + + func disconnect(from endpointID: EndpointID) { + connectionManager.disconnect(from: endpointID) + } + + func sendBytes(to endpointIDs: [EndpointID]) { + let payloadID = PayloadID.unique() + let token = connectionManager.send(Config.bytePayload.data(using: .utf8)!, to: endpointIDs, id: payloadID) + let payload = Payload( + id: payloadID, + type: .bytes, + status: .inProgress(Progress()), + isIncoming: false, + cancellationToken: token + ) + for endpointID in endpointIDs { + guard let index = connections.firstIndex(where: { $0.id == endpointID }) else { + return + } + connections[index].payloads.insert(payload, at: 0) + } + } +} + +extension Model: DiscovererDelegate { + func discoverer(_ discoverer: Discoverer, didFind endpointID: EndpointID, with context: Data) { + let endpoint = DiscoveredEndpoint( + id: endpointID, + endpointName: String(data: context, encoding: .utf8)! + ) + endpoints.insert(endpoint, at: 0) + } + + func discoverer(_ discoverer: Discoverer, didLose endpointID: EndpointID) { + guard let index = endpoints.firstIndex(where: { $0.id == endpointID }) else { + return + } + endpoints.remove(at: index) + } +} + +extension Model: AdvertiserDelegate { + func advertiser(_ advertiser: Advertiser, didReceiveConnectionRequestFrom endpointID: EndpointID, with context: Data, connectionRequestHandler: @escaping (Bool) -> Void) { + let endpoint = DiscoveredEndpoint( + id: endpointID, + endpointName: String(data: context, encoding: .utf8)! + ) + endpoints.insert(endpoint, at: 0) + connectionRequestHandler(true) + } +} + +extension Model: ConnectionManagerDelegate { + func connectionManager(_ connectionManager: ConnectionManager, didReceive verificationCode: String, from endpointID: EndpointID, verificationHandler: @escaping (Bool) -> Void) { + guard let index = endpoints.firstIndex(where: { $0.id == endpointID }) else { + return + } + let endpoint = endpoints.remove(at: index) + let request = ConnectionRequest( + id: endpointID, + endpointName: endpoint.endpointName, + pin: verificationCode, + shouldAccept: { accept in + verificationHandler(accept) + } + ) + requests.insert(request, at: 0) + } + + func connectionManager(_ connectionManager: ConnectionManager, didReceive data: Data, withID payloadID: PayloadID, from endpointID: EndpointID) { + let payload = Payload( + id: payloadID, + type: .bytes, + status: .success, + isIncoming: true, + cancellationToken: nil + ) + guard let index = connections.firstIndex(where: { $0.id == endpointID }) else { + return + } + connections[index].payloads.insert(payload, at: 0) + } + + func connectionManager(_ connectionManager: ConnectionManager, didReceive stream: InputStream, withID payloadID: PayloadID, from endpointID: EndpointID, cancellationToken token: CancellationToken) { + let payload = Payload( + id: payloadID, + type: .stream, + status: .success, + isIncoming: true, + cancellationToken: token + ) + guard let index = connections.firstIndex(where: { $0.id == endpointID }) else { + return + } + connections[index].payloads.insert(payload, at: 0) + } + + func connectionManager(_ connectionManager: ConnectionManager, didStartReceivingResourceWithID payloadID: PayloadID, from endpointID: EndpointID, at localURL: URL, withName name: String, cancellationToken token: CancellationToken) { + let payload = Payload( + id: payloadID, + type: .file, + status: .inProgress(Progress()), + isIncoming: true, + cancellationToken: token + ) + guard let index = connections.firstIndex(where: { $0.id == endpointID }) else { + return + } + connections[index].payloads.insert(payload, at: 0) + } + + func connectionManager(_ connectionManager: ConnectionManager, didReceiveTransferUpdate update: TransferUpdate, from endpointID: EndpointID, forPayload payloadID: PayloadID) { + guard let connectionIndex = connections.firstIndex(where: { $0.id == endpointID }), + let payloadIndex = connections[connectionIndex].payloads.firstIndex(where: { $0.id == payloadID }) else { + return + } + switch update { + case .success: + connections[connectionIndex].payloads[payloadIndex].status = .success + case .canceled: + connections[connectionIndex].payloads[payloadIndex].status = .canceled + case .failure: + connections[connectionIndex].payloads[payloadIndex].status = .failure + case let .progress(progress): + connections[connectionIndex].payloads[payloadIndex].status = .inProgress(progress) + } + } + + func connectionManager(_ connectionManager: ConnectionManager, didChangeTo state: ConnectionState, for endpointID: EndpointID) { + switch (state) { + case .connecting: + break + case .connected: + guard let index = requests.firstIndex(where: { $0.id == endpointID }) else { + return + } + let request = requests.remove(at: index) + let connection = ConnectedEndpoint( + id: endpointID, + endpointName: request.endpointName + ) + connections.insert(connection, at: 0) + case .disconnected: + guard let index = connections.firstIndex(where: { $0.id == endpointID }) else { + return + } + connections.remove(at: index) + case .rejected: + guard let index = requests.firstIndex(where: { $0.id == endpointID }) else { + return + } + requests.remove(at: index) + } + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Model/Payload.swift b/connections/swift/NearbyConnections/Example/iOS Example/Model/Payload.swift new file mode 100644 index 00000000..a3eaf1d9 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Model/Payload.swift @@ -0,0 +1,33 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NearbyConnections + +struct Payload: Identifiable { + let id: PayloadID + var type: PayloadType + var status: Status + let isIncoming: Bool + let cancellationToken: CancellationToken? + + enum PayloadType { + case bytes, stream, file + } + enum Status { + case inProgress(Progress), success, failure, canceled + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/PayloadView.swift b/connections/swift/NearbyConnections/Example/iOS Example/PayloadView.swift new file mode 100644 index 00000000..5ec63b38 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/PayloadView.swift @@ -0,0 +1,121 @@ +// +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PayloadView: View { + var payload: Payload + + var body: some View { + HStack(alignment: .top) { + Image(systemName: payload.isIncoming + ? "square.and.arrow.down" + : "square.and.arrow.up") + VStack(alignment: .leading) { + switch payload.type { + case .bytes: + Text("Bytes") + case .file: + Text("File") + case .stream: + Text("Stream") + } + HStack { + switch payload.status { + case let .inProgress(progress): + Image(systemName: "xmark.circle.fill").onTapGesture { + payload.cancellationToken?.cancel() + }.foregroundColor(.secondary) + ProgressView(value: Float(progress.completedUnitCount), total: Float(progress.totalUnitCount)) + case .canceled: + Text("Canceled") + .font(.caption) + .foregroundColor(.secondary) + case .failure: + Text("Failed") + .font(.caption) + .foregroundColor(.secondary) + case .success: + Text(payload.isIncoming ? "Received" : "Sent") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + } + } +} + +struct PayloadView_Previews: PreviewProvider { + static var previews: some View { + List { + PayloadView( + payload: Payload( + id: 0, + type: .bytes, + status: .success, + isIncoming: false, + cancellationToken: nil + ) + ) + PayloadView( + payload: Payload( + id: 0, + type: .bytes, + status: .success, + isIncoming: true, + cancellationToken: nil + ) + ) + PayloadView( + payload: Payload( + id: 0, + type: .bytes, + status: .canceled, + isIncoming: true, + cancellationToken: nil + ) + ) + PayloadView( + payload: Payload( + id: 0, + type: .bytes, + status: .failure, + isIncoming: true, + cancellationToken: nil + ) + ) + PayloadView( + payload: Payload( + id: 0, + type: .file, + status: .inProgress(Progress(totalUnitCount: 100)), + isIncoming: true, + cancellationToken: nil + ) + ) + PayloadView( + payload: Payload( + id: 0, + type: .stream, + status: .inProgress(Progress()), + isIncoming: true, + cancellationToken: nil + ) + ) + } + } +} diff --git a/connections/swift/NearbyConnections/Example/iOS Example/Preview Content/Preview Assets.xcassets/Contents.json b/connections/swift/NearbyConnections/Example/iOS Example/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/connections/swift/NearbyConnections/Example/iOS Example/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} From 562b5a8c5960b740bf40f71de93478ad7d98cec4 Mon Sep 17 00:00:00 2001 From: Nick Bourdakos Date: Sat, 1 Apr 2023 15:23:01 -0400 Subject: [PATCH 2/4] Remove .DS_Store --- connections/.DS_Store | Bin 6148 -> 0 bytes .../swift/NearbyConnections/Example/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 connections/.DS_Store delete mode 100644 connections/swift/NearbyConnections/Example/.DS_Store diff --git a/connections/.DS_Store b/connections/.DS_Store deleted file mode 100644 index 0ca2b44eb803e7db8d406f9abc63aa67ea3180eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJBk895UtV?1QWvz`wHHm5k0|Nz(96ckP-Z*6M3#Z+P?ZE!(gVtF0Y{abywHx zre82MB_g`M?pGp95t+jc5r~noCR|;seX*X*;DQoNKaaL;!di80h5~3oFMTo)me- Y=Gd=^eW243cRG+i1Evd&3cOo^U%xdJ=Kufz diff --git a/connections/swift/NearbyConnections/Example/.DS_Store b/connections/swift/NearbyConnections/Example/.DS_Store deleted file mode 100644 index f5391216b8cfb231acaac848f4f08ec6720e05ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!AiqG5Pe&Fh#wWM-Fr3z;1NVe(jxff0a=DmXZ#`9$QqXio;_84^QUV~Qu-phks%abA|SH`vh}t@8S$N1J1y|V}LbVBs)@c>kK#p&cKEN`9DOeU~bqcre6n5 zY5|Dyj7-9@^b(R24RgazkzXi5Q;C`yti=eLPJgtx+^|#BbOdWYg6$lvLkVejt{+o4 zLayl68E^)68931CNb3Le>;8W?$!E@hGw`Pv2*YAlOn4-#t-ZrZt&P+ Date: Sat, 1 Apr 2023 15:25:02 -0400 Subject: [PATCH 3/4] Add .DS_Store to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 48464321..39b82caf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + build/** .build From 59de13edd513a05a5664faeb38ef7b252789dfca Mon Sep 17 00:00:00 2001 From: Nick Bourdakos Date: Sat, 1 Apr 2023 15:26:04 -0400 Subject: [PATCH 4/4] Remove development team --- .../Example/iOS Example.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.pbxproj b/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.pbxproj index 92052d7b..aaab107c 100644 --- a/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.pbxproj +++ b/connections/swift/NearbyConnections/Example/iOS Example.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"iOS Example/Preview Content\""; - DEVELOPMENT_TEAM = SAW3M8QNN9; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -371,7 +371,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"iOS Example/Preview Content\""; - DEVELOPMENT_TEAM = SAW3M8QNN9; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;