├── screenshot.png ├── MarchThirteen ├── MarchThirteen.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── MarchThirteen.xcscheme │ └── project.pbxproj └── MarchThirteen │ ├── WalkieTalkie │ ├── CommunicatorOutput.swift │ ├── Peer.swift │ ├── CommunicatorPayload.swift │ ├── CommunicatorMessage.swift │ ├── Communicator.swift │ └── Service.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Utils │ └── UIImageExtension.swift │ ├── ChatRoom.swift │ └── ViewController.swift ├── MarchThirteen.xcworkspace └── contents.xcworkspacedata ├── LICENSE ├── Podfile.lock ├── .gitignore ├── Podfile └── README.md /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zweigraf/march-thirteen/HEAD/screenshot.png -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MarchThirteen.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/WalkieTalkie/CommunicatorOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommunicatorOutput.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 18.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol CommunicatorOutput { 12 | associatedtype PayloadType: CommunicatorPayload 13 | 14 | init(from: CommunicatorMessage) 15 | } 16 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/WalkieTalkie/Peer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Peer.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 18.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MultipeerConnectivity 11 | 12 | /// Represents one peer. 13 | public struct Peer { 14 | /// Name that should be used when displaying this peer. 15 | let name: String 16 | } 17 | 18 | // MARK: - MCPeerID compatibility 19 | internal extension Peer { 20 | init(peerID: MCPeerID) { 21 | self.init(name: peerID.displayName) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 13.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SuperDelegate 11 | 12 | @UIApplicationMain 13 | class AppDelegate: SuperDelegate, ApplicationLaunched { 14 | var window: UIWindow? 15 | 16 | func setupApplication() { 17 | 18 | } 19 | 20 | func loadInterface(launchItem: LaunchItem) { 21 | let window = UIWindow() 22 | let navigationController = UINavigationController(rootViewController: ViewController()) 23 | window.rootViewController = navigationController 24 | setup(mainWindow: window) 25 | self.window = window 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/WalkieTalkie/CommunicatorPayload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommunicatorPayload.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 18.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol CommunicatorPayload { 12 | /// Initializes a new object from the given dictionary. Should return nil 13 | /// if the dictionary cannot be decoded. 14 | /// 15 | /// - Parameters: 16 | /// - from: A dictionary representation retrieved via `dictionaryRepresentation` 17 | /// of the same type. 18 | init?(from dictionary: [String: Any]) 19 | 20 | /// Encodes this object as a dictionary. Dictionary layout can vary, but 21 | /// this type should be able to decode that dictionary via `init?(from:)` 22 | /// into an instance of itself. The values & keys in the dictionary must be 23 | /// NSCoding compliant. 24 | var dictionaryRepresentation: [String: Any] { get } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Luis Reisewitz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - JSQMessagesViewController (7.3.4) 3 | - LambdaKit (0.0.3) 4 | - PureLayout (3.0.2) 5 | - Sensitive (3.0) 6 | - SuperDelegate (0.9.0) 7 | 8 | DEPENDENCIES: 9 | - JSQMessagesViewController (from `https://github.com/jessesquires/JSQMessagesViewController.git`, branch `develop`) 10 | - LambdaKit (~> 0.0.3) 11 | - PureLayout (~> 3.0.2) 12 | - Sensitive (~> 3.0) 13 | - SuperDelegate (~> 0.9.0) 14 | 15 | EXTERNAL SOURCES: 16 | JSQMessagesViewController: 17 | :branch: develop 18 | :git: https://github.com/jessesquires/JSQMessagesViewController.git 19 | 20 | CHECKOUT OPTIONS: 21 | JSQMessagesViewController: 22 | :commit: 45fdac769d7354ad19e5990dadbfee285a2e45ee 23 | :git: https://github.com/jessesquires/JSQMessagesViewController.git 24 | 25 | SPEC CHECKSUMS: 26 | JSQMessagesViewController: c11b9e77372ab72c45c67311f6da7342e32df1e8 27 | LambdaKit: 87d603283c5915077fc09a8cbba03b4c2a48d2bf 28 | PureLayout: 4d550abe49a94f24c2808b9b95db9131685fe4cd 29 | Sensitive: dee25cff31a5420e9fa4d4e2f0e1cef8a46f701f 30 | SuperDelegate: b70c7afddf8525217d0e074b850b8ea3cc6f8abd 31 | 32 | PODFILE CHECKSUM: feaef47745e6df85072aee06b7875154afef9ac3 33 | 34 | COCOAPODS: 1.2.0 35 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | project 'MarchThirteen/MarchThirteen.xcodeproj' 2 | 3 | # Uncomment this line to define a global platform for your project 4 | platform :ios, '10.0' 5 | 6 | target 'MarchThirteen' do 7 | # Comment this line if you're not using Swift and don't want to use dynamic frameworks 8 | use_frameworks! 9 | 10 | # Pods for MarchThirteen 11 | 12 | # SuperDelegate provides a clean application delegate interface and protects you from bugs in the application lifecycle 13 | # https://github.com/square/SuperDelegate 14 | pod 'SuperDelegate', '~> 0.9.0' 15 | 16 | # The ultimate API for iOS & OS X Auto Layout — impressively simple, immensely powerful. Objective-C and Swift compatible. 17 | # https://github.com/PureLayout/PureLayout 18 | pod 'PureLayout', '~> 3.0.2' 19 | 20 | # Fresh look at work with gestures in Swift. 21 | # https://github.com/igormatyushkin014/Sensitive 22 | pod 'Sensitive', '~> 3.0' 23 | 24 | # An elegant messages UI library for iOS 25 | # https://github.com/jessesquires/JSQMessagesViewController 26 | pod 'JSQMessagesViewController', :git => 'https://github.com/jessesquires/JSQMessagesViewController.git', :branch => 'develop' 27 | 28 | # Modern Swift API for NSUserDefaults 29 | # https://github.com/radex/SwiftyUserDefaults 30 | # Uncomment to use 31 | # pod 'SwiftyUserDefaults', '~> 3.0.0' 32 | 33 | # A clean and lightweight progress HUD for your iOS and tvOS app. 34 | # https://github.com/SVProgressHUD/SVProgressHUD 35 | # Uncomment to use 36 | # pod 'SVProgressHUD', '~> 2.1.2' 37 | 38 | # Closures on most used UIKit methods 39 | # https://github.com/Reflejo/LambdaKit 40 | pod 'LambdaKit', '~> 0.0.3' 41 | 42 | # Intuitive date handling in Swift 43 | # https://github.com/naoty/Timepiece 44 | # Uncomment to use 45 | # pod 'Timepiece', '~> 1.1.0' 46 | 47 | # Crash reporting & Analytics 48 | # https://fabric.io/kits/ios/crashlytics/install 49 | # Uncomment to use 50 | # pod 'Fabric', '~> 1.6.11' 51 | # pod 'Crashlytics', '~> 3.8.3' 52 | end 53 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/Utils/UIImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageExtension.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 21.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreGraphics 11 | 12 | public extension UIImage { 13 | static func initialsImage(for string: String, size: CGSize = CGSize(width: 30, height: 30)) -> UIImage? { 14 | let initials = string.components(separatedBy: CharacterSet.whitespaces.union(.punctuationCharacters)) 15 | .map { 16 | let startIndex = $0.startIndex 17 | let secondIndex = $0.index(after: startIndex) 18 | return $0.substring(to: secondIndex) 19 | }.joined().uppercased() 20 | 21 | UIGraphicsBeginImageContextWithOptions(size, true, UIScreen.main.scale) 22 | guard let context = UIGraphicsGetCurrentContext() else { 23 | return nil 24 | } 25 | 26 | context.setFillColor(UIColor.darkGray.cgColor) 27 | context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) 28 | 29 | let paragraphStyle = NSMutableParagraphStyle() 30 | paragraphStyle.alignment = .center 31 | let attrs = [NSFontAttributeName: UIFont(name: "HelveticaNeue-Bold", size: 12)!, 32 | NSParagraphStyleAttributeName: paragraphStyle, 33 | NSForegroundColorAttributeName: UIColor.white] 34 | 35 | let attributedString = NSAttributedString(string: initials, attributes: attrs) 36 | let stringSize = attributedString.size() 37 | let xOffset = (size.width - stringSize.width) / 2 38 | let yOffset = (size.height - stringSize.height) / 2 39 | 40 | attributedString.draw(in: CGRect(x: xOffset, y: yOffset, width: stringSize.width, height: stringSize.height)) 41 | 42 | guard let cgImage = context.makeImage() else { 43 | return nil 44 | } 45 | return UIImage(cgImage: cgImage) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/WalkieTalkie/CommunicatorMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommunicatorMessage.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 18.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | fileprivate enum DictionaryKeys: String { 12 | case meta 13 | case payload 14 | } 15 | 16 | /// Represents a message. 17 | public struct CommunicatorMessage { 18 | /// The payload of the message as sent by the other party. 19 | public let payload: PayloadType 20 | /// The peer that sent this message. 21 | public let peer: Peer 22 | 23 | // TODO: add timestamps & figure out way to synchronise devices times 24 | } 25 | 26 | // MARK: - ↔️ Data In/Out ↔️ 27 | internal extension CommunicatorMessage { 28 | private init?(from dictionary: [String : Any], peer: Peer) { 29 | guard let payloadDict = dictionary[DictionaryKeys.payload.rawValue] as? [String: Any], 30 | let payload = PayloadType(from: payloadDict) else { 31 | return nil 32 | } 33 | self.init(payload: payload, peer: peer) 34 | } 35 | 36 | private var dictionaryRepresentation: [String : Any] { 37 | // Wrap payload in dictionary to later attach metadata for ourselves 38 | // Do not serialise the peer, that will not be transferred. 39 | let dictionary: [String: Any] = [ 40 | DictionaryKeys.payload.rawValue: payload.dictionaryRepresentation 41 | ] 42 | return dictionary 43 | } 44 | 45 | init?(from data: Data, peer: Peer) { 46 | guard let dictionary = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Any] else { 47 | return nil 48 | } 49 | self.init(from: dictionary, peer: peer) 50 | } 51 | 52 | var dataRepresentation: Data { 53 | let dictionary = dictionaryRepresentation 54 | let data = NSKeyedArchiver.archivedData(withRootObject: dictionary) 55 | return data 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # march-thirteen 2 | Local Chat Application with MultiPeerConnectivity. 3 | 4 | ## How it works 5 | 6 | The app opens a service via MultiPeerConnectivity and at the same time advertises 7 | itself as a peer and browses for other peers. If any peers are found, invitations 8 | are sent out automatically. Received invitations are accepted automatically. 9 | 10 | Messages can then be send to all connected peers, sort of like a group chat. 11 | 12 | No additional networking logic on top of MultiPeerConnectivity is implemented (yet). 13 | 14 | ## Screenshot 15 | 16 | ![screenshot](screenshot.png) 17 | 18 | ## WalkieTalkie 19 | 20 | All of the networking logic is contained in the WalkieTalkie folder which might 21 | later be refactored into a separate library. 22 | 23 | The main component is the `Communicator` class. This is a generic class 24 | encapsulating networking. You use this to send and receive messages. 25 | 26 | Messages are serialized & deserialized using a combination of generic protocols. 27 | 28 | `Communicator` is specialized on the `CommunicatorOutput` protocol. This can be 29 | anything you like it to, e.g. in this application it represents a `ChatEvent`. 30 | The `CommunicatorOutput` protocol also defines a payload type, which is the actual 31 | object that will be sent over the wire. 32 | 33 | Serialization is done via the `CommunicatorPayload` protocol. A payload has to 34 | know how to represent itself as a dictionary and how to construct itself from 35 | such a dictionary. 36 | 37 | This dictionary is then sent over the wire and arrives on the other side. There 38 | the `Communicator` creates a `CommunicatorMessage` object, which includes the 39 | deserialized payload and some meta information (like the associated peer). 40 | 41 | This message object is used to instantiate an `CommunicatorOutput` instance 42 | which takes the message and transforms it into an application level object. 43 | 44 | This all plays together to create a seamless serialisation & deserialisation logic 45 | without any action needed on the calling side. 46 | 47 | ### Example Usage 48 | 49 | For a concrete usage example please see the `ChatRoom.swift` file. There all the 50 | application level objects for the chat are defined implementing the needed 51 | protocols. -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/WalkieTalkie/Communicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Communicator.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 18.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import MultipeerConnectivity 12 | 13 | // MARK: - Type Def 14 | public class Communicator { 15 | // MARK: Public Properties 16 | var messageReceived: MessageReceivedCallback? 17 | var messageSent: MessageSentCallback? 18 | var peersChanged: PeersChangedCallback? 19 | 20 | init(identifier: String, ownPeerID: String) { 21 | let peerID = MCPeerID(displayName: ownPeerID) 22 | self.ownPeerID = peerID 23 | 24 | service = Service(with: identifier, as: peerID) 25 | 26 | service.dataReceived = didReceive 27 | service.peersChanged = peersChanged 28 | 29 | service.start() 30 | } 31 | 32 | // MARK: Type Aliases 33 | fileprivate typealias PayloadType = OutputType.PayloadType 34 | typealias Message = CommunicatorMessage 35 | typealias Output = OutputType 36 | typealias MessageReceivedCallback = (OutputType) -> Void 37 | typealias MessageSentCallback = (OutputType) -> Void 38 | typealias PeersChangedCallback = ([Peer]) -> Void 39 | 40 | // MARK: Private Properties 41 | fileprivate let service: Service 42 | fileprivate let ownPeerID: MCPeerID 43 | } 44 | 45 | // MARK: - ⬆️ Sending ⬆️ 46 | public extension Communicator { 47 | func send(message: PayloadType) { 48 | let message = Message(payload: message, peer: Peer(peerID: ownPeerID)) 49 | let data = message.dataRepresentation 50 | do { 51 | try service.send(data: data) 52 | let output = OutputType(from: message) 53 | messageSent?(output) 54 | } catch {} 55 | } 56 | } 57 | 58 | // MARK: - 📞 Callbacks 📞 59 | fileprivate extension Communicator { 60 | func didReceive(data: Data, from peer: MCPeerID) { 61 | guard let message = Message(from: data, peer: Peer(peerID: peer)) else { 62 | return 63 | } 64 | let output = OutputType(from: message) 65 | messageReceived?(output) 66 | } 67 | 68 | func peersChanged(peers: [MCPeerID]) { 69 | let peers = peers.map { Peer(peerID: $0) } 70 | peersChanged?(peers) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen.xcodeproj/xcshareddata/xcschemes/MarchThirteen.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/ChatRoom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatRoom.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 18.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Payload { 12 | case typingStarted 13 | case message(text: String) 14 | } 15 | 16 | extension Payload: CommunicatorPayload { 17 | init?(from dictionary: [String : Any]) { 18 | guard let type = dictionary["type"] as? String else { 19 | return nil 20 | } 21 | 22 | switch type { 23 | case "typingStarted": 24 | self = .typingStarted 25 | case "message": 26 | guard let message = dictionary["message"] as? String else { 27 | return nil 28 | } 29 | self = .message(text: message) 30 | default: 31 | return nil 32 | } 33 | } 34 | 35 | var dictionaryRepresentation: [String : Any] { 36 | switch self { 37 | case .typingStarted: 38 | return ["type": "typingStarted"] 39 | case .message(let text): 40 | return ["type": "message", "message": text] 41 | } 42 | } 43 | } 44 | 45 | // Could also be an enum 46 | class ChatEvent: NSObject, CommunicatorOutput { 47 | typealias PayloadType = Payload 48 | 49 | let event: PayloadType 50 | let username: String 51 | 52 | init(event: PayloadType, username: String) { 53 | self.event = event 54 | self.username = username 55 | super.init() 56 | } 57 | 58 | required init(from message: CommunicatorMessage) { 59 | event = message.payload 60 | username = message.peer.name 61 | super.init() 62 | } 63 | } 64 | 65 | class ChatRoom { 66 | fileprivate lazy var communicator: Communicator = { 67 | return Communicator(identifier: "marchthirteen", ownPeerID: self.ownName) 68 | }() 69 | 70 | fileprivate var typingEvents = [ChatEvent]() { 71 | didSet { 72 | typingStatusChanged?(typingEvents.count > 0) 73 | } 74 | } 75 | 76 | let ownName = UIDevice.current.name 77 | var messages = [ChatEvent]() 78 | 79 | var messagesChanged: (() -> Void)? 80 | var usersChanged: (([String]) -> Void)? 81 | var typingStatusChanged: ((Bool) -> Void)? 82 | 83 | init() { 84 | communicator.messageReceived = receive 85 | communicator.messageSent = receive 86 | communicator.peersChanged = peersChanged 87 | } 88 | 89 | func send(message: String) { 90 | communicator.send(message: .message(text: message)) 91 | } 92 | 93 | func sendStartTyping() { 94 | communicator.send(message: .typingStarted) 95 | } 96 | 97 | func receive(message: ChatEvent) { 98 | switch message.event { 99 | case .typingStarted: 100 | let weAlreadyHaveThisEvent = typingEvents.filter { 101 | $0.username == message.username 102 | }.count > 0 103 | // Do not count our own typing events or already existing ones 104 | guard message.username != ownName, 105 | !weAlreadyHaveThisEvent else { return } 106 | typingEvents.append(message) 107 | case .message: 108 | // Only keep typing events where the username is not that of 109 | // the received message (assume that user has finished typing) 110 | typingEvents = typingEvents.filter { 111 | $0.username != message.username 112 | } 113 | messages.append(message) 114 | messagesChanged?() 115 | } 116 | } 117 | 118 | func peersChanged(peers: [Peer]) { 119 | let userList = peers.map { $0.name } 120 | usersChanged?(userList) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 13.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PureLayout 11 | import Sensitive 12 | import JSQMessagesViewController 13 | 14 | class ViewController: JSQMessagesViewController { 15 | let startTime = mach_absolute_time() 16 | let incomingBubble = JSQMessagesBubbleImageFactory().incomingMessagesBubbleImage(with: UIColor(red: 10/255, green: 180/255, blue: 230/255, alpha: 1.0)) 17 | let outgoingBubble = JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(with: UIColor.lightGray) 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | automaticallyScrollsToMostRecentMessage = true 23 | chatRoom.messagesChanged = messagesChanged 24 | chatRoom.typingStatusChanged = typingStatusChanged 25 | chatRoom.usersChanged = usersChanged 26 | } 27 | 28 | // MARK: Model 29 | let chatRoom = ChatRoom() 30 | } 31 | 32 | // MARK: - JSQMessagesViewController Overrides 33 | extension ViewController { 34 | override func senderId() -> String { 35 | return chatRoom.ownName 36 | } 37 | 38 | override func senderDisplayName() -> String { 39 | return chatRoom.ownName 40 | } 41 | 42 | override func didPressSend(_ button: UIButton, withMessageText text: String, senderId: String, senderDisplayName: String, date: Date) { 43 | chatRoom.send(message: text) 44 | finishSendingMessage(animated: true) 45 | } 46 | 47 | override func textViewDidChange(_ textView: UITextView) { 48 | super.textViewDidChange(textView) 49 | // Send start typing message when the textfield has one character 50 | guard textView.text?.characters.count == 1 else { return } 51 | chatRoom.sendStartTyping() 52 | } 53 | 54 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 55 | return chatRoom.messages.count 56 | } 57 | 58 | override func collectionView(_ collectionView: JSQMessagesCollectionView, messageDataForItemAt indexPath: IndexPath) -> JSQMessageData { 59 | let data = chatRoom.messages[indexPath.row] 60 | let text: String 61 | if case let .message(messageText) = data.event { 62 | text = messageText 63 | } else { 64 | text = "" 65 | } 66 | return JSQMessage(senderId: data.username, displayName: data.username, text: text) 67 | } 68 | 69 | override func collectionView(_ collectionView: JSQMessagesCollectionView, messageBubbleImageDataForItemAt indexPath: IndexPath) -> JSQMessageBubbleImageDataSource? { 70 | let data = self.collectionView(collectionView, messageDataForItemAt: indexPath) 71 | switch(data.senderId()) { 72 | case senderId(): 73 | return self.outgoingBubble 74 | default: 75 | return self.incomingBubble 76 | } 77 | } 78 | 79 | override func collectionView(_ collectionView: JSQMessagesCollectionView, avatarImageDataForItemAt indexPath: IndexPath) -> JSQMessageAvatarImageDataSource? { 80 | let data = chatRoom.messages[indexPath.row] 81 | guard let image = UIImage.initialsImage(for: data.username) else { 82 | return nil 83 | } 84 | return JSQMessagesAvatarImage(avatarImage: image, highlightedImage: nil, placeholderImage: image) 85 | } 86 | } 87 | 88 | // MARK: - Chat Room Callbacks 89 | extension ViewController { 90 | func messagesChanged() { 91 | DispatchQueue.main.async { 92 | self.finishReceivingMessage(animated: true) 93 | } 94 | } 95 | 96 | func typingStatusChanged(shouldShowTypingIndicator: Bool) { 97 | DispatchQueue.main.async { 98 | self.showTypingIndicator = shouldShowTypingIndicator 99 | } 100 | } 101 | 102 | func usersChanged(users: [String]) { 103 | DispatchQueue.main.async { 104 | self.title = users.joined(separator: ", ") 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen/WalkieTalkie/Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Service.swift 3 | // MarchThirteen 4 | // 5 | // Created by Luis Reisewitz on 14.03.17. 6 | // Copyright © 2017 ZweiGraf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MultipeerConnectivity 11 | 12 | enum ServiceError: Error { 13 | case noConnectedPeers 14 | } 15 | 16 | internal class Service: NSObject { 17 | // MARK: 🔓 Public Properties 🔓 18 | 19 | typealias PeersChangedCallback = (_ peers: [MCPeerID]) -> Void 20 | typealias DataReceivedCallback = (_ data: Data, _ peer: MCPeerID) -> Void 21 | 22 | /// This callback will be called when the list of connected peers changes. 23 | var peersChanged: PeersChangedCallback? 24 | /// This callback will be called when the session receives data from one of 25 | /// the connected peers. 26 | var dataReceived: DataReceivedCallback? 27 | 28 | /// Initializes the service manager with a given service type and peerID. 29 | /// 30 | /// - Parameters: 31 | /// - type: A MultiPeer service type. Service type must be a unique 32 | /// string, at most 15 characters long and can contain only ASCII lowercase 33 | /// letters, numbers and hyphens. 34 | /// - peerID: The user's own peer id to be shown to others. 35 | init(with type: String, as peerID: MCPeerID) { 36 | self.type = type 37 | self.ownPeer = peerID 38 | super.init() 39 | } 40 | 41 | // MARK: 🔒 Private Properties 🔒 42 | 43 | /// The type that is used for advertising and browsing the service. 44 | fileprivate let type: String 45 | 46 | /// The list of currently connected peers (peers in state MCSessionState.connected). 47 | fileprivate var connectedPeers: [MCPeerID] { 48 | return session.connectedPeers 49 | } 50 | 51 | /// The peer id of the local user. 52 | fileprivate let ownPeer: MCPeerID 53 | 54 | /// Session used for communicating with peers. 55 | fileprivate lazy var session: MCSession = { 56 | let session = MCSession( 57 | peer: self.ownPeer, securityIdentity: nil, encryptionPreference: .none) 58 | session.delegate = self 59 | return session 60 | }() 61 | 62 | /// Advertises our peer to others. 63 | fileprivate lazy var serviceAdvertiser: MCNearbyServiceAdvertiser = { 64 | let advertiser = MCNearbyServiceAdvertiser( 65 | peer: self.ownPeer, discoveryInfo: nil, serviceType: self.type) 66 | advertiser.delegate = self 67 | return advertiser 68 | }() 69 | 70 | /// Browses for other peers. 71 | fileprivate lazy var serviceBrowser: MCNearbyServiceBrowser = { 72 | let browser = MCNearbyServiceBrowser( 73 | peer: self.ownPeer, serviceType: self.type) 74 | browser.delegate = self 75 | return browser 76 | }() 77 | 78 | deinit { 79 | // Stop MultiPeer Stuff 80 | stop() 81 | } 82 | 83 | } 84 | 85 | // MARK: - 🔓 Public API 🔓 86 | extension Service { 87 | /// Starts advertising & browsing for our service. 88 | func start() { 89 | serviceAdvertiser.startAdvertisingPeer() 90 | serviceBrowser.startBrowsingForPeers() 91 | } 92 | 93 | /// Stops advertising & browsing for our service. 94 | func stop() { 95 | serviceAdvertiser.stopAdvertisingPeer() 96 | serviceBrowser.stopBrowsingForPeers() 97 | } 98 | 99 | /// Tries to send given data to all connected peers. 100 | /// 101 | /// - Parameter data: data to be sent to peers. 102 | func send(data: Data) throws { 103 | guard session.connectedPeers.count > 0 else { 104 | throw ServiceError.noConnectedPeers 105 | } 106 | try session.send( 107 | data, toPeers: session.connectedPeers, with: .reliable) 108 | } 109 | } 110 | 111 | // MARK: - 🔒 Private Delegate Processing 🔒 112 | fileprivate extension Service { 113 | func notifyReceive(data: Data, from peer: MCPeerID) { 114 | dataReceived?(data, peer) 115 | } 116 | 117 | func notifyPeersChanged(peers: [MCPeerID]) { 118 | peersChanged?(peers) 119 | } 120 | } 121 | 122 | // MARK: - MCNearbyServiceAdvertiserDelegate 123 | extension Service: MCNearbyServiceAdvertiserDelegate { 124 | func advertiser( 125 | _ advertiser: MCNearbyServiceAdvertiser, 126 | didReceiveInvitationFromPeer peerID: MCPeerID, 127 | withContext context: Data?, 128 | invitationHandler: @escaping (Bool, MCSession?) -> Void) { 129 | 130 | // Automatically accept all invitations that we get 131 | invitationHandler(true, session) 132 | } 133 | 134 | // NOOPs 135 | 136 | func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) { 137 | // TODO: Handle advertising start errors 138 | } 139 | } 140 | 141 | // MARK: - MCNearbyServiceBrowserDelegate 142 | extension Service: MCNearbyServiceBrowserDelegate { 143 | func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) { 144 | // Automatically invite all peers we find 145 | browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10) 146 | } 147 | 148 | // NOOPs 149 | 150 | func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { 151 | // TODO: handle pending / invitable peers 152 | } 153 | func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) { 154 | // TODO: Handle browsing start errors 155 | } 156 | } 157 | 158 | // MARK: - MCSessionDelegate 159 | extension Service: MCSessionDelegate { 160 | func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { 161 | notifyPeersChanged(peers: connectedPeers) 162 | } 163 | 164 | func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { 165 | notifyReceive(data: data, from: peerID) 166 | } 167 | 168 | // NOOPs 169 | 170 | func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {} 171 | func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {} 172 | func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL, withError error: Error?) {} 173 | } 174 | -------------------------------------------------------------------------------- /MarchThirteen/MarchThirteen.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A517CD811E77555600F67CC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A517CD801E77555600F67CC0 /* AppDelegate.swift */; }; 11 | A517CD831E77555600F67CC0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A517CD821E77555600F67CC0 /* ViewController.swift */; }; 12 | A517CD881E77555600F67CC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A517CD871E77555600F67CC0 /* Assets.xcassets */; }; 13 | A517CD8B1E77555600F67CC0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A517CD891E77555600F67CC0 /* LaunchScreen.storyboard */; }; 14 | A561081E1E80A84D00FB50C1 /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A561081D1E80A84D00FB50C1 /* UIImageExtension.swift */; }; 15 | A5BD327E1E7D9B310071E63F /* ChatRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BD327D1E7D9B310071E63F /* ChatRoom.swift */; }; 16 | A5BD32891E7D9DB50071E63F /* Communicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BD32871E7D9DB50071E63F /* Communicator.swift */; }; 17 | A5BD328A1E7D9DB50071E63F /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BD32881E7D9DB50071E63F /* Service.swift */; }; 18 | A5BD328C1E7D9E490071E63F /* CommunicatorPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BD328B1E7D9E490071E63F /* CommunicatorPayload.swift */; }; 19 | A5BD328E1E7D9F760071E63F /* Peer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BD328D1E7D9F760071E63F /* Peer.swift */; }; 20 | A5BD32901E7D9FB20071E63F /* CommunicatorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BD328F1E7D9FB20071E63F /* CommunicatorMessage.swift */; }; 21 | A5BD32921E7DB6EC0071E63F /* CommunicatorOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BD32911E7DB6EC0071E63F /* CommunicatorOutput.swift */; }; 22 | DBFB395BD78A6DE9EEBC9069 /* Pods_MarchThirteen.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C81374F6A4B9C409EB7E531 /* Pods_MarchThirteen.framework */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 28997B1D764EEC218D7A1857 /* Pods-MarchThirteen.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MarchThirteen.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-MarchThirteen/Pods-MarchThirteen.debug.xcconfig"; sourceTree = ""; }; 27 | 8C81FE065521EC7EF892FD53 /* Pods-MarchThirteen.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MarchThirteen.release.xcconfig"; path = "../Pods/Target Support Files/Pods-MarchThirteen/Pods-MarchThirteen.release.xcconfig"; sourceTree = ""; }; 28 | 9C81374F6A4B9C409EB7E531 /* Pods_MarchThirteen.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MarchThirteen.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | A517CD7D1E77555600F67CC0 /* MarchThirteen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MarchThirteen.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | A517CD801E77555600F67CC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 31 | A517CD821E77555600F67CC0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 32 | A517CD871E77555600F67CC0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | A517CD8A1E77555600F67CC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 34 | A517CD8C1E77555600F67CC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | A561081D1E80A84D00FB50C1 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; 36 | A5BD327D1E7D9B310071E63F /* ChatRoom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatRoom.swift; sourceTree = ""; }; 37 | A5BD32871E7D9DB50071E63F /* Communicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Communicator.swift; sourceTree = ""; }; 38 | A5BD32881E7D9DB50071E63F /* Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; 39 | A5BD328B1E7D9E490071E63F /* CommunicatorPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunicatorPayload.swift; sourceTree = ""; }; 40 | A5BD328D1E7D9F760071E63F /* Peer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peer.swift; sourceTree = ""; }; 41 | A5BD328F1E7D9FB20071E63F /* CommunicatorMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunicatorMessage.swift; sourceTree = ""; }; 42 | A5BD32911E7DB6EC0071E63F /* CommunicatorOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunicatorOutput.swift; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | A517CD7A1E77555600F67CC0 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | DBFB395BD78A6DE9EEBC9069 /* Pods_MarchThirteen.framework in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 67EFB15B27D9DF441D71692E /* Pods */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 28997B1D764EEC218D7A1857 /* Pods-MarchThirteen.debug.xcconfig */, 61 | 8C81FE065521EC7EF892FD53 /* Pods-MarchThirteen.release.xcconfig */, 62 | ); 63 | name = Pods; 64 | sourceTree = ""; 65 | }; 66 | 8CE1B4671E21297CCEB772CD /* Frameworks */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 9C81374F6A4B9C409EB7E531 /* Pods_MarchThirteen.framework */, 70 | ); 71 | name = Frameworks; 72 | sourceTree = ""; 73 | }; 74 | A517CD741E77555600F67CC0 = { 75 | isa = PBXGroup; 76 | children = ( 77 | A517CD7F1E77555600F67CC0 /* MarchThirteen */, 78 | A517CD7E1E77555600F67CC0 /* Products */, 79 | 67EFB15B27D9DF441D71692E /* Pods */, 80 | 8CE1B4671E21297CCEB772CD /* Frameworks */, 81 | ); 82 | sourceTree = ""; 83 | }; 84 | A517CD7E1E77555600F67CC0 /* Products */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | A517CD7D1E77555600F67CC0 /* MarchThirteen.app */, 88 | ); 89 | name = Products; 90 | sourceTree = ""; 91 | }; 92 | A517CD7F1E77555600F67CC0 /* MarchThirteen */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | A561081C1E80A81F00FB50C1 /* Utils */, 96 | A5BD32861E7D9DB50071E63F /* WalkieTalkie */, 97 | A517CD801E77555600F67CC0 /* AppDelegate.swift */, 98 | A517CD821E77555600F67CC0 /* ViewController.swift */, 99 | A517CD871E77555600F67CC0 /* Assets.xcassets */, 100 | A517CD891E77555600F67CC0 /* LaunchScreen.storyboard */, 101 | A517CD8C1E77555600F67CC0 /* Info.plist */, 102 | A5BD327D1E7D9B310071E63F /* ChatRoom.swift */, 103 | ); 104 | path = MarchThirteen; 105 | sourceTree = ""; 106 | }; 107 | A561081C1E80A81F00FB50C1 /* Utils */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | A561081D1E80A84D00FB50C1 /* UIImageExtension.swift */, 111 | ); 112 | path = Utils; 113 | sourceTree = ""; 114 | }; 115 | A5BD32861E7D9DB50071E63F /* WalkieTalkie */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | A5BD32871E7D9DB50071E63F /* Communicator.swift */, 119 | A5BD32881E7D9DB50071E63F /* Service.swift */, 120 | A5BD328B1E7D9E490071E63F /* CommunicatorPayload.swift */, 121 | A5BD328D1E7D9F760071E63F /* Peer.swift */, 122 | A5BD328F1E7D9FB20071E63F /* CommunicatorMessage.swift */, 123 | A5BD32911E7DB6EC0071E63F /* CommunicatorOutput.swift */, 124 | ); 125 | path = WalkieTalkie; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | A517CD7C1E77555600F67CC0 /* MarchThirteen */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = A517CD8F1E77555600F67CC0 /* Build configuration list for PBXNativeTarget "MarchThirteen" */; 134 | buildPhases = ( 135 | D2CB2ABC38B0411185AC341C /* [CP] Check Pods Manifest.lock */, 136 | A517CD791E77555600F67CC0 /* Sources */, 137 | A517CD7A1E77555600F67CC0 /* Frameworks */, 138 | A517CD7B1E77555600F67CC0 /* Resources */, 139 | 2A98BBA1C8B33651DF36C794 /* [CP] Embed Pods Frameworks */, 140 | 35F214CE6C748DEBCC9E7DA3 /* [CP] Copy Pods Resources */, 141 | ); 142 | buildRules = ( 143 | ); 144 | dependencies = ( 145 | ); 146 | name = MarchThirteen; 147 | productName = MarchThirteen; 148 | productReference = A517CD7D1E77555600F67CC0 /* MarchThirteen.app */; 149 | productType = "com.apple.product-type.application"; 150 | }; 151 | /* End PBXNativeTarget section */ 152 | 153 | /* Begin PBXProject section */ 154 | A517CD751E77555600F67CC0 /* Project object */ = { 155 | isa = PBXProject; 156 | attributes = { 157 | LastSwiftUpdateCheck = 0820; 158 | LastUpgradeCheck = 0820; 159 | ORGANIZATIONNAME = ZweiGraf; 160 | TargetAttributes = { 161 | A517CD7C1E77555600F67CC0 = { 162 | CreatedOnToolsVersion = 8.2.1; 163 | ProvisioningStyle = Automatic; 164 | }; 165 | }; 166 | }; 167 | buildConfigurationList = A517CD781E77555600F67CC0 /* Build configuration list for PBXProject "MarchThirteen" */; 168 | compatibilityVersion = "Xcode 3.2"; 169 | developmentRegion = English; 170 | hasScannedForEncodings = 0; 171 | knownRegions = ( 172 | en, 173 | Base, 174 | ); 175 | mainGroup = A517CD741E77555600F67CC0; 176 | productRefGroup = A517CD7E1E77555600F67CC0 /* Products */; 177 | projectDirPath = ""; 178 | projectRoot = ""; 179 | targets = ( 180 | A517CD7C1E77555600F67CC0 /* MarchThirteen */, 181 | ); 182 | }; 183 | /* End PBXProject section */ 184 | 185 | /* Begin PBXResourcesBuildPhase section */ 186 | A517CD7B1E77555600F67CC0 /* Resources */ = { 187 | isa = PBXResourcesBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | A517CD8B1E77555600F67CC0 /* LaunchScreen.storyboard in Resources */, 191 | A517CD881E77555600F67CC0 /* Assets.xcassets in Resources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXResourcesBuildPhase section */ 196 | 197 | /* Begin PBXShellScriptBuildPhase section */ 198 | 2A98BBA1C8B33651DF36C794 /* [CP] Embed Pods Frameworks */ = { 199 | isa = PBXShellScriptBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | ); 203 | inputPaths = ( 204 | ); 205 | name = "[CP] Embed Pods Frameworks"; 206 | outputPaths = ( 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | shellPath = /bin/sh; 210 | shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-MarchThirteen/Pods-MarchThirteen-frameworks.sh\"\n"; 211 | showEnvVarsInLog = 0; 212 | }; 213 | 35F214CE6C748DEBCC9E7DA3 /* [CP] Copy Pods Resources */ = { 214 | isa = PBXShellScriptBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | ); 218 | inputPaths = ( 219 | ); 220 | name = "[CP] Copy Pods Resources"; 221 | outputPaths = ( 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | shellPath = /bin/sh; 225 | shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-MarchThirteen/Pods-MarchThirteen-resources.sh\"\n"; 226 | showEnvVarsInLog = 0; 227 | }; 228 | D2CB2ABC38B0411185AC341C /* [CP] Check Pods Manifest.lock */ = { 229 | isa = PBXShellScriptBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | ); 233 | inputPaths = ( 234 | ); 235 | name = "[CP] Check Pods Manifest.lock"; 236 | outputPaths = ( 237 | ); 238 | runOnlyForDeploymentPostprocessing = 0; 239 | shellPath = /bin/sh; 240 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 241 | showEnvVarsInLog = 0; 242 | }; 243 | /* End PBXShellScriptBuildPhase section */ 244 | 245 | /* Begin PBXSourcesBuildPhase section */ 246 | A517CD791E77555600F67CC0 /* Sources */ = { 247 | isa = PBXSourcesBuildPhase; 248 | buildActionMask = 2147483647; 249 | files = ( 250 | A5BD328C1E7D9E490071E63F /* CommunicatorPayload.swift in Sources */, 251 | A517CD831E77555600F67CC0 /* ViewController.swift in Sources */, 252 | A5BD32901E7D9FB20071E63F /* CommunicatorMessage.swift in Sources */, 253 | A561081E1E80A84D00FB50C1 /* UIImageExtension.swift in Sources */, 254 | A517CD811E77555600F67CC0 /* AppDelegate.swift in Sources */, 255 | A5BD328A1E7D9DB50071E63F /* Service.swift in Sources */, 256 | A5BD32891E7D9DB50071E63F /* Communicator.swift in Sources */, 257 | A5BD327E1E7D9B310071E63F /* ChatRoom.swift in Sources */, 258 | A5BD328E1E7D9F760071E63F /* Peer.swift in Sources */, 259 | A5BD32921E7DB6EC0071E63F /* CommunicatorOutput.swift in Sources */, 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | /* End PBXSourcesBuildPhase section */ 264 | 265 | /* Begin PBXVariantGroup section */ 266 | A517CD891E77555600F67CC0 /* LaunchScreen.storyboard */ = { 267 | isa = PBXVariantGroup; 268 | children = ( 269 | A517CD8A1E77555600F67CC0 /* Base */, 270 | ); 271 | name = LaunchScreen.storyboard; 272 | sourceTree = ""; 273 | }; 274 | /* End PBXVariantGroup section */ 275 | 276 | /* Begin XCBuildConfiguration section */ 277 | A517CD8D1E77555600F67CC0 /* Debug */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | CLANG_ANALYZER_NONNULL = YES; 282 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 283 | CLANG_CXX_LIBRARY = "libc++"; 284 | CLANG_ENABLE_MODULES = YES; 285 | CLANG_ENABLE_OBJC_ARC = YES; 286 | CLANG_WARN_BOOL_CONVERSION = YES; 287 | CLANG_WARN_CONSTANT_CONVERSION = YES; 288 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 289 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 290 | CLANG_WARN_EMPTY_BODY = YES; 291 | CLANG_WARN_ENUM_CONVERSION = YES; 292 | CLANG_WARN_INFINITE_RECURSION = YES; 293 | CLANG_WARN_INT_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 296 | CLANG_WARN_UNREACHABLE_CODE = YES; 297 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 298 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 299 | COPY_PHASE_STRIP = NO; 300 | DEBUG_INFORMATION_FORMAT = dwarf; 301 | ENABLE_STRICT_OBJC_MSGSEND = YES; 302 | ENABLE_TESTABILITY = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu99; 304 | GCC_DYNAMIC_NO_PIC = NO; 305 | GCC_NO_COMMON_BLOCKS = YES; 306 | GCC_OPTIMIZATION_LEVEL = 0; 307 | GCC_PREPROCESSOR_DEFINITIONS = ( 308 | "DEBUG=1", 309 | "$(inherited)", 310 | ); 311 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 312 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 313 | GCC_WARN_UNDECLARED_SELECTOR = YES; 314 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 315 | GCC_WARN_UNUSED_FUNCTION = YES; 316 | GCC_WARN_UNUSED_VARIABLE = YES; 317 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 318 | MTL_ENABLE_DEBUG_INFO = YES; 319 | ONLY_ACTIVE_ARCH = YES; 320 | SDKROOT = iphoneos; 321 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 322 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 323 | TARGETED_DEVICE_FAMILY = "1,2"; 324 | }; 325 | name = Debug; 326 | }; 327 | A517CD8E1E77555600F67CC0 /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ALWAYS_SEARCH_USER_PATHS = NO; 331 | CLANG_ANALYZER_NONNULL = YES; 332 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 333 | CLANG_CXX_LIBRARY = "libc++"; 334 | CLANG_ENABLE_MODULES = YES; 335 | CLANG_ENABLE_OBJC_ARC = YES; 336 | CLANG_WARN_BOOL_CONVERSION = YES; 337 | CLANG_WARN_CONSTANT_CONVERSION = YES; 338 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 339 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 340 | CLANG_WARN_EMPTY_BODY = YES; 341 | CLANG_WARN_ENUM_CONVERSION = YES; 342 | CLANG_WARN_INFINITE_RECURSION = YES; 343 | CLANG_WARN_INT_CONVERSION = YES; 344 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 345 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 346 | CLANG_WARN_UNREACHABLE_CODE = YES; 347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 348 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 349 | COPY_PHASE_STRIP = NO; 350 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 351 | ENABLE_NS_ASSERTIONS = NO; 352 | ENABLE_STRICT_OBJC_MSGSEND = YES; 353 | GCC_C_LANGUAGE_STANDARD = gnu99; 354 | GCC_NO_COMMON_BLOCKS = YES; 355 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 356 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 357 | GCC_WARN_UNDECLARED_SELECTOR = YES; 358 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 359 | GCC_WARN_UNUSED_FUNCTION = YES; 360 | GCC_WARN_UNUSED_VARIABLE = YES; 361 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 362 | MTL_ENABLE_DEBUG_INFO = NO; 363 | SDKROOT = iphoneos; 364 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | VALIDATE_PRODUCT = YES; 367 | }; 368 | name = Release; 369 | }; 370 | A517CD901E77555600F67CC0 /* Debug */ = { 371 | isa = XCBuildConfiguration; 372 | baseConfigurationReference = 28997B1D764EEC218D7A1857 /* Pods-MarchThirteen.debug.xcconfig */; 373 | buildSettings = { 374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 375 | DEVELOPMENT_TEAM = ""; 376 | INFOPLIST_FILE = MarchThirteen/Info.plist; 377 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 378 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 379 | PRODUCT_BUNDLE_IDENTIFIER = com.zweigraf.MarchThirteen; 380 | PRODUCT_NAME = "$(TARGET_NAME)"; 381 | SWIFT_VERSION = 3.0; 382 | }; 383 | name = Debug; 384 | }; 385 | A517CD911E77555600F67CC0 /* Release */ = { 386 | isa = XCBuildConfiguration; 387 | baseConfigurationReference = 8C81FE065521EC7EF892FD53 /* Pods-MarchThirteen.release.xcconfig */; 388 | buildSettings = { 389 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 390 | DEVELOPMENT_TEAM = ""; 391 | INFOPLIST_FILE = MarchThirteen/Info.plist; 392 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 393 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 394 | PRODUCT_BUNDLE_IDENTIFIER = com.zweigraf.MarchThirteen; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | SWIFT_VERSION = 3.0; 397 | }; 398 | name = Release; 399 | }; 400 | /* End XCBuildConfiguration section */ 401 | 402 | /* Begin XCConfigurationList section */ 403 | A517CD781E77555600F67CC0 /* Build configuration list for PBXProject "MarchThirteen" */ = { 404 | isa = XCConfigurationList; 405 | buildConfigurations = ( 406 | A517CD8D1E77555600F67CC0 /* Debug */, 407 | A517CD8E1E77555600F67CC0 /* Release */, 408 | ); 409 | defaultConfigurationIsVisible = 0; 410 | defaultConfigurationName = Release; 411 | }; 412 | A517CD8F1E77555600F67CC0 /* Build configuration list for PBXNativeTarget "MarchThirteen" */ = { 413 | isa = XCConfigurationList; 414 | buildConfigurations = ( 415 | A517CD901E77555600F67CC0 /* Debug */, 416 | A517CD911E77555600F67CC0 /* Release */, 417 | ); 418 | defaultConfigurationIsVisible = 0; 419 | defaultConfigurationName = Release; 420 | }; 421 | /* End XCConfigurationList section */ 422 | }; 423 | rootObject = A517CD751E77555600F67CC0 /* Project object */; 424 | } 425 | --------------------------------------------------------------------------------