版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.12.01 星期二 |
前言
MultipeerConnectivity
框架支持点对点连接和附近设备的发现。是iOS 7 推出的众多新框架的一种,它拓宽了操作系统中应用的范围。其目的是使开发者可以创建通过Wi-Fi或蓝牙在近距离建立连接的应用。是在近距离设备间建立互动,交换数据和其他资源的很好的简单工具。接下来几篇我们就一起看一下这个框架。感兴趣的可以看下面几篇文章。
1. MultipeerConnectivity框架详细解析(一) —— 基本概览(一)
2. MultipeerConnectivity框架详细解析(二) —— 一个简单示例(一)
源码
1. Swift
首先看下工程组织结构
下面就是源码啦
1. SceneDelegate.swift
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
}
}
2. ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
JobListView()
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "briefcase")
Text("Jobs")
}
NavigationView {
JoinSessionView()
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "bubble.left")
Text("Messages")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
3. JobConnectionManager.swift
import Foundation
import MultipeerConnectivity
class JobConnectionManager: NSObject, ObservableObject {
typealias JobReceivedHandler = (JobModel) -> Void
private static let service = "jobmanager-jobs"
@Published var employees: [MCPeerID] = []
private var session: MCSession
private let myPeerId = MCPeerID(displayName: UIDevice.current.name)
private var nearbyServiceBrowser: MCNearbyServiceBrowser
private var nearbyServiceAdvertiser: MCNearbyServiceAdvertiser
private let jobReceivedHandler: JobReceivedHandler?
private var jobToSend: JobModel?
private var peerInvitee: MCPeerID?
init(_ jobReceivedHandler: JobReceivedHandler? = nil) {
session = MCSession(peer: myPeerId, securityIdentity: nil, encryptionPreference: .none)
nearbyServiceAdvertiser = MCNearbyServiceAdvertiser(
peer: myPeerId,
discoveryInfo: nil,
serviceType: JobConnectionManager.service)
nearbyServiceBrowser = MCNearbyServiceBrowser(peer: myPeerId, serviceType: JobConnectionManager.service)
self.jobReceivedHandler = jobReceivedHandler
super.init()
session.delegate = self
nearbyServiceAdvertiser.delegate = self
nearbyServiceBrowser.delegate = self
}
func startBrowsing() {
nearbyServiceBrowser.startBrowsingForPeers()
}
func stopBrowsing() {
nearbyServiceBrowser.stopBrowsingForPeers()
}
var isReceivingJobs: Bool = false {
didSet {
if isReceivingJobs {
nearbyServiceAdvertiser.startAdvertisingPeer()
} else {
nearbyServiceAdvertiser.stopAdvertisingPeer()
}
}
}
func invitePeer(_ peerID: MCPeerID, to job: JobModel) {
jobToSend = job
let context = job.name.data(using: .utf8)
nearbyServiceBrowser.invitePeer(peerID, to: session, withContext: context, timeout: TimeInterval(120))
}
private func send(_ job: JobModel, to peer: MCPeerID) {
do {
let data = try JSONEncoder().encode(job)
try session.send(data, toPeers: [peer], with: .reliable)
} catch {
print(error.localizedDescription)
}
}
}
extension JobConnectionManager: MCNearbyServiceAdvertiserDelegate {
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
guard
let window = UIApplication.shared.windows.first,
let context = context,
let jobName = String(data: context, encoding: .utf8)
else { return }
let title = "Accept \(peerID.displayName)'s Job"
let message = "Would you like to accept: \(jobName)"
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
alertController.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
invitationHandler(true, self.session)
})
window.rootViewController?.present(alertController, animated: true)
}
}
extension JobConnectionManager: MCNearbyServiceBrowserDelegate {
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
if !employees.contains(peerID) {
employees.append(peerID)
}
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
guard let index = employees.firstIndex(of: peerID) else { return }
employees.remove(at: index)
}
}
extension JobConnectionManager: MCSessionDelegate {
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
switch state {
case .connected:
guard let jobToSend = jobToSend else { return }
send(jobToSend, to: peerID)
case .notConnected:
print("Not connected: \(peerID.displayName)")
case .connecting:
print("Connecting to: \(peerID.displayName)")
@unknown default:
print("Unknown state: \(state)")
}
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
guard let job = try? JSONDecoder().decode(JobModel.self, from: data) else { return }
DispatchQueue.main.async {
self.jobReceivedHandler?(job)
}
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {}
}
4. ChatConnectionManager.swift
import Foundation
import MultipeerConnectivity
class ChatConnectionManager: NSObject, ObservableObject {
private static let service = "jobmanager-chat"
@Published var messages: [ChatMessage] = []
@Published var peers: [MCPeerID] = []
@Published var connectedToChat = false
let myPeerId = MCPeerID(displayName: UIDevice.current.name)
private var advertiserAssistant: MCNearbyServiceAdvertiser?
private var session: MCSession?
private var isHosting = false
func send(_ message: String) {
let chatMessage = ChatMessage(displayName: myPeerId.displayName, body: message)
messages.append(chatMessage)
guard
let session = session,
let data = message.data(using: .utf8),
!session.connectedPeers.isEmpty
else { return }
do {
try session.send(data, toPeers: session.connectedPeers, with: .reliable)
} catch {
print(error.localizedDescription)
}
}
func sendHistory(to peer: MCPeerID) {
let tempFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("messages.data")
guard let historyData = try? JSONEncoder().encode(messages) else { return }
try? historyData.write(to: tempFile)
session?.sendResource(at: tempFile, withName: "Chat_History", toPeer: peer) { error in
if let error = error {
print(error.localizedDescription)
}
}
}
func join() {
peers.removeAll()
messages.removeAll()
session = MCSession(peer: myPeerId, securityIdentity: nil, encryptionPreference: .required)
session?.delegate = self
guard
let window = UIApplication.shared.windows.first,
let session = session
else { return }
let mcBrowserViewController = MCBrowserViewController(serviceType: ChatConnectionManager.service, session: session)
mcBrowserViewController.delegate = self
window.rootViewController?.present(mcBrowserViewController, animated: true)
}
func host() {
isHosting = true
peers.removeAll()
messages.removeAll()
connectedToChat = true
session = MCSession(peer: myPeerId, securityIdentity: nil, encryptionPreference: .required)
session?.delegate = self
advertiserAssistant = MCNearbyServiceAdvertiser(
peer: myPeerId,
discoveryInfo: nil,
serviceType: ChatConnectionManager.service)
advertiserAssistant?.delegate = self
advertiserAssistant?.startAdvertisingPeer()
}
func leaveChat() {
isHosting = false
connectedToChat = false
advertiserAssistant?.stopAdvertisingPeer()
messages.removeAll()
session = nil
advertiserAssistant = nil
}
}
extension ChatConnectionManager: MCNearbyServiceAdvertiserDelegate {
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
invitationHandler(true, session)
}
}
extension ChatConnectionManager: MCSessionDelegate {
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
guard let message = String(data: data, encoding: .utf8) else { return }
let chatMessage = ChatMessage(displayName: peerID.displayName, body: message)
DispatchQueue.main.async {
self.messages.append(chatMessage)
}
}
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
switch state {
case .connected:
if !peers.contains(peerID) {
DispatchQueue.main.async {
self.peers.insert(peerID, at: 0)
}
if isHosting {
sendHistory(to: peerID)
}
}
case .notConnected:
DispatchQueue.main.async {
if let index = self.peers.firstIndex(of: peerID) {
self.peers.remove(at: index)
}
if self.peers.isEmpty && !self.isHosting {
self.connectedToChat = false
}
}
case .connecting:
print("Connecting to: \(peerID.displayName)")
@unknown default:
print("Unknown state: \(state)")
}
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
print("Receiving chat history")
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
guard
let localURL = localURL,
let data = try? Data(contentsOf: localURL),
let messages = try? JSONDecoder().decode([ChatMessage].self, from: data)
else { return }
DispatchQueue.main.async {
self.messages.insert(contentsOf: messages, at: 0)
}
}
}
extension ChatConnectionManager: MCBrowserViewControllerDelegate {
func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
browserViewController.dismiss(animated: true) {
self.connectedToChat = true
}
}
func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
session?.disconnect()
browserViewController.dismiss(animated: true)
}
}
5. JobModel.swift
import Foundation
class JobListStore: ObservableObject {
@Published var jobs: [JobModel] = []
}
struct JobModel: Codable, Identifiable {
var id = UUID()
let name: String
let dueDate: Date
let payout: String
init(name: String, dueDate: Date, payout: String) {
let payoutNumber = NSNumber(value: Int(payout) ?? 0)
let payoutString = NumberFormatter.currency.string(from: payoutNumber) ?? ""
self.name = name
self.dueDate = dueDate
self.payout = payoutString
}
func data() -> Data? {
let encoder = JSONEncoder()
return try? encoder.encode(self)
}
}
6. ChatMessage.swift
import UIKit
struct ChatMessage: Identifiable, Equatable, Codable {
var id = UUID()
let displayName: String
let body: String
var time = Date()
var isUser: Bool {
return displayName == UIDevice.current.name
}
}
7. JobListView.swift
import SwiftUI
struct JobListView: View {
@ObservedObject var jobListStore: JobListStore
@ObservedObject var jobConnectionManager: JobConnectionManager
@State private var showAddJob = false
init(jobListStore: JobListStore = JobListStore()) {
self.jobListStore = jobListStore
jobConnectionManager = JobConnectionManager { job in
jobListStore.jobs.append(job)
}
}
var body: some View {
List {
Section(
header: headerView,
footer: footerView) {
ForEach(jobListStore.jobs) { job in
JobListRowView(job: job)
.environmentObject(jobConnectionManager)
}
.onDelete { indexSet in
jobListStore.jobs.remove(atOffsets: indexSet)
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Jobs")
.sheet(isPresented: $showAddJob) {
NavigationView {
AddJobView()
.environmentObject(jobListStore)
}
}
}
var headerView: some View {
Toggle("Receive Jobs", isOn: $jobConnectionManager.isReceivingJobs)
}
var footerView: some View {
Button(
action: {
showAddJob = true
}, label: {
Label("Add Job", systemImage: "plus.circle")
})
.buttonStyle(FooterButtonStyle())
}
}
#if DEBUG
struct JobListViewPreview: PreviewProvider {
static var previews: some View {
NavigationView {
JobListView(jobListStore: JobListStore())
}
}
}
#endif
8. JobListRowView.swift
import SwiftUI
struct JobListRowView: View {
@EnvironmentObject var jobConnectionManager: JobConnectionManager
let job: JobModel
var body: some View {
NavigationLink(
destination:
JobView(job: job)
.environmentObject(jobConnectionManager)) {
HStack {
VStack(alignment: .leading) {
Text(job.name)
.font(.title3)
Text("\(job.dueDate, formatter: DateFormatter.dueDateFormatter)")
.font(.caption)
}
Spacer()
Text(job.payout)
.font(.headline)
}
}
}
}
#if DEBUG
struct JobListRowView_Previews: PreviewProvider {
static var previews: some View {
JobListRowView(job: JobModel(name: "Mock Job", dueDate: Date(), payout: "$25.00"))
.environmentObject(JobConnectionManager())
}
}
#endif
9. JobView.swift
import SwiftUI
struct JobView: View {
let job: JobModel
@EnvironmentObject var jobConnectionManager: JobConnectionManager
var body: some View {
List {
HStack {
Label(
title: {
Text("Due Date")
.font(.headline)
},
icon: {
Image(systemName: "calendar")
})
Spacer()
Text("\(job.dueDate, formatter: DateFormatter.dueDateFormatter)")
}
HStack {
Label(
title: {
Text("Payout")
.font(.headline)
},
icon: {
Image(systemName: "creditcard")
})
Spacer()
Text(job.payout)
}
Section(
header: HStack(spacing: 8) {
Text("Available Employees")
Spacer()
ProgressView()
}) {
ForEach(jobConnectionManager.employees, id: \.self) { employee in
HStack {
Text(employee.displayName)
.font(.headline)
Spacer()
Image(systemName: "arrowshape.turn.up.right.fill")
}
.onTapGesture {
jobConnectionManager.invitePeer(employee, to: job)
}
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(job.name)
.onAppear {
jobConnectionManager.startBrowsing()
}
.onDisappear {
jobConnectionManager.stopBrowsing()
}
}
}
#if DEBUG
struct JobView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
JobView(job: JobModel(name: "Test Job", dueDate: Date(), payout: "$25.00"))
.environmentObject(JobConnectionManager())
}
}
}
#endif
10. AddJobView.swift
import SwiftUI
struct AddJobView: View {
@EnvironmentObject var jobListStore: JobListStore
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var jobName = ""
@State private var dueDate = Date()
@State private var payout = ""
var body: some View {
Form {
TextField("Job Name", text: $jobName)
DatePicker("Due Date", selection: $dueDate, in: Date()..., displayedComponents: .date)
HStack {
Text(NumberFormatter.currency.currencySymbol)
TextField("Payout", text: $payout)
.keyboardType(.numberPad)
}
Button("Save") {
let job = JobModel(name: jobName, dueDate: dueDate, payout: payout)
jobListStore.jobs.append(job)
presentationMode.wrappedValue.dismiss()
}
.disabled(jobName.isEmpty || payout.isEmpty)
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle("Add Job", displayMode: .inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
}
#if DEBUG
struct AddJobView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
AddJobView()
.environmentObject(JobListStore())
}
}
}
#endif
11. JoinSessionView.swift
import SwiftUI
struct JoinSessionView: View {
@ObservedObject private var chatConnectionManager = ChatConnectionManager()
var body: some View {
VStack(spacing: 24) {
Image(systemName: "network")
.resizable()
.frame(width: 100, height: 100)
Button(
action: {
chatConnectionManager.join()
}, label: {
Label("Join a Chat Session", systemImage: "arrow.up.right.and.arrow.down.left.rectangle")
})
.buttonStyle(MultipeerButtonStyle())
Button(
action: {
chatConnectionManager.host()
}, label: {
Label("Host a Chat Session", systemImage: "plus.circle")
})
.buttonStyle(MultipeerButtonStyle())
NavigationLink(
destination: ChatView()
.environmentObject(chatConnectionManager),
isActive: $chatConnectionManager.connectedToChat) {
EmptyView()
}
}
.navigationTitle("Chat")
}
}
#if DEBUG
struct JoinSessionView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
JoinSessionView()
}
}
}
#endif
12. ChatView.swift
import SwiftUI
struct ChatView: View {
@EnvironmentObject var chatConnectionManager: ChatConnectionManager
@State private var messageText = ""
var body: some View {
VStack {
chatInfoView
ChatListView()
.environmentObject(chatConnectionManager)
messageField
}
.navigationBarTitle("Chat", displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Leave") {
chatConnectionManager.leaveChat()
}
}
}
.navigationBarBackButtonHidden(true)
}
private var messageField: some View {
VStack(spacing: 0) {
Divider()
// swiftlint:disable:next trailing_closure
TextField("Enter Message", text: $messageText, onCommit: {
guard !messageText.isEmpty else { return }
chatConnectionManager.send(messageText)
messageText = ""
})
.padding()
}
}
private var chatInfoView: some View {
VStack(alignment: .leading) {
Divider()
HStack {
Text("People in chat:")
.fixedSize(horizontal: true, vertical: false)
.font(.headline)
if chatConnectionManager.peers.isEmpty {
Text("Empty")
.font(Font.caption.italic())
.foregroundColor(Color("rw-dark"))
} else {
chatParticipants
}
}
.padding(.top, 8)
.padding(.leading, 16)
Divider()
}
.frame(height: 44)
}
private var chatParticipants: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(chatConnectionManager.peers, id: \.self) { peer in
Text(peer.displayName)
.padding(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/, 6)
.background(Color("rw-dark"))
.foregroundColor(.white)
.font(Font.body.bold())
.cornerRadius(9)
}
}
}
}
}
#if DEBUG
import MultipeerConnectivity
struct ChatView_Previews: PreviewProvider {
static let chatConnectionManager = ChatConnectionManager()
static var previews: some View {
NavigationView {
ChatView()
.environmentObject(chatConnectionManager)
.onAppear {
chatConnectionManager.peers.append(MCPeerID(displayName: "Test Peer"))
}
}
}
}
#endif
13. ChatListView.swift
import SwiftUI
struct ChatListView: View {
@EnvironmentObject var chatConnectionManager: ChatConnectionManager
var body: some View {
ScrollView {
ScrollViewReader { reader in
VStack(alignment: .leading, spacing: 20) {
ForEach(chatConnectionManager.messages) { message in
MessageBodyView(message: message)
.onAppear {
if message == chatConnectionManager.messages.last {
reader.scrollTo(message.id)
}
}
}
}
.padding(16)
}
}
.background(Color(UIColor.systemBackground))
}
}
#if DEBUG
struct ChatListView_Previews: PreviewProvider {
static var previews: some View {
ChatListView()
.environmentObject(ChatConnectionManager())
}
}
#endif
14. MessageBodyView.swift
import SwiftUI
struct MessageBodyView: View {
let message: ChatMessage
var body: some View {
HStack {
if message.isUser {
Spacer()
}
VStack(alignment: message.isUser ? .trailing : .leading, spacing: 4) {
Text(message.body)
.font(.body)
.padding(8)
.foregroundColor(.white)
.background(message.isUser ? .green : Color("rw-dark"))
.cornerRadius(9)
TimestampView(message: message)
}
}
}
}
#if DEBUG
struct MessageBodyView_Previews: PreviewProvider {
static var previews: some View {
VStack {
MessageBodyView(message: ChatMessage(displayName: "User 1", body: "Test"))
MessageBodyView(message: ChatMessage(displayName: UIDevice.current.name, body: "Test"))
}
.padding()
}
}
#endif
15. TimestampView.swift
import SwiftUI
struct TimestampView: View {
let message: ChatMessage
var body: some View {
HStack(spacing: 2) {
Text(message.displayName)
Text("@")
Text("\(message.time, formatter: DateFormatter.timestampFormatter)")
if !message.isUser {
Spacer()
}
}
.font(.caption)
.foregroundColor(Color("rw-dark"))
}
}
#if DEBUG
struct TimestampView_Previews: PreviewProvider {
static var previews: some View {
VStack {
TimestampView(message: ChatMessage(displayName: "User 1", body: "Test"))
TimestampView(message: ChatMessage(displayName: UIDevice.current.name, body: "Test"))
}
.padding()
}
}
#endif
16. ButtonStyles.swift
import SwiftUI
struct MultipeerButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.font(.headline)
.background(configuration.isPressed ? Color("rw-dark") : Color.accentColor)
.cornerRadius(9.0)
.foregroundColor(.white)
}
}
struct ChatMessageButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
Spacer()
configuration.label
Spacer()
}
.padding(8)
.background(configuration.isPressed ? Color("rw-dark") : Color.green)
.cornerRadius(9.0)
.foregroundColor(.white)
}
}
struct FooterButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ? Color("rw-dark") : .accentColor)
.font(.headline)
.padding(8)
}
}
17. Formatters.swift
import Foundation
extension NumberFormatter {
static var currency: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
return numberFormatter
}()
}
extension DateFormatter {
static var dueDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
static var timestampFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
}
后记
本篇主要讲述了
MultipeerConnectivity
的一个简单示例,感兴趣的给个赞或者关注~~~
网友评论