美文网首页
MultipeerConnectivity框架详细解析(三) —

MultipeerConnectivity框架详细解析(三) —

作者: 刀客传奇 | 来源:发表于2020-12-01 21:49 被阅读0次

版本记录

版本号 时间
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的一个简单示例,感兴趣的给个赞或者关注~~~

相关文章

网友评论

      本文标题:MultipeerConnectivity框架详细解析(三) —

      本文链接:https://www.haomeiwen.com/subject/wudewktx.html