Skip to content

Commit

Permalink
homework_8
Browse files Browse the repository at this point in the history
  • Loading branch information
nikita-bor committed Nov 14, 2017
2 parents b6a8a0c + b205bec commit 4ed8902
Show file tree
Hide file tree
Showing 36 changed files with 835 additions and 656 deletions.
80 changes: 36 additions & 44 deletions TinkoffChat.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions TinkoffChat/Core Layer/IdentifierGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// IdentifierGenerator.swift
// TinkoffChat
//
// Created by Nikita Borodulin on 10/11/2017.
// Copyright © 2017 com.nikitaborodulin. All rights reserved.
//

import Foundation

protocol IIdentifierGenerator: class {
func generateIdentifier() -> String
}

class IdentifierGenerator: IIdentifierGenerator {
func generateIdentifier() -> String {
return ("\(arc4random_uniform(UINT32_MAX)) + \(Date.timeIntervalSinceReferenceDate) + \(arc4random_uniform(UINT32_MAX))".data(using: .utf8)?.base64EncodedString())!
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import MultipeerConnectivity

protocol ICommunicator: class {
func sendMessage(text: String, to userID: String, completionHandler: ((_ success: Bool, _ error: Error?) -> Void)?)
func sendMessage(text: String, to userID: String) throws
weak var delegate: ICommunicatorDelegate? { get set }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ class MessageEncoder: IMessageEncoder {
private static let messageIdKey = "messageId"
private static let messageTextKey = "text"

private let identifierGenerator: IIdentifierGenerator

init(_ identifierGenerator: IIdentifierGenerator) {
self.identifierGenerator = identifierGenerator
}

func prepareForSend(text: String) -> Data? {
let message = [MessageEncoder.messageEventTypeKey: MessageEncoder.messageEventTypeDescription,
MessageEncoder.messageIdKey: generateIdentifier(),
MessageEncoder.messageIdKey: identifierGenerator.generateIdentifier(),
MessageEncoder.messageTextKey: text]
do {
let messageData = try JSONSerialization.data(withJSONObject: message, options: .prettyPrinted)
Expand All @@ -30,8 +36,4 @@ class MessageEncoder: IMessageEncoder {
}
return nil
}

private func generateIdentifier() -> String {
return ("\(arc4random_uniform(UINT32_MAX)) + \(Date.timeIntervalSinceReferenceDate) + \(arc4random_uniform(UINT32_MAX))".data(using: .utf8)?.base64EncodedString())!
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class MultipeerCommunicator: NSObject {
enum MultipeerCommunicatorError: Error {
case communicatorInternalError
case communicatorMessageEncodingError
case communicatorCantSend
}

weak var delegate: ICommunicatorDelegate?
Expand All @@ -27,6 +26,7 @@ class MultipeerCommunicator: NSObject {

private var sessionsByPeerID = [MCPeerID: MCSession]()
private var foundPeers = [MCPeerID: [String: String]?]()
private var connectedPeers = Set<MCPeerID>()

init(_ messageHandler: IMessageHandler) {
self.messageHandler = messageHandler
Expand Down Expand Up @@ -71,20 +71,17 @@ class MultipeerCommunicator: NSObject {
// MARK: ICommunicator

extension MultipeerCommunicator: ICommunicator {
func sendMessage(text: String, to userID: String, completionHandler: ((_ success: Bool, _ error: Error?) -> Void)?) {
func sendMessage(text: String, to userID: String) throws {
guard let peerID = getPeerIDFor(userID), let session = sessionsByPeerID[peerID] else {
completionHandler?(false, MultipeerCommunicatorError.communicatorInternalError)
return
throw MultipeerCommunicatorError.communicatorInternalError
}
guard let serializedMessage = messageHandler.prepareForSend(text: text) else {
completionHandler?(false, MultipeerCommunicatorError.communicatorMessageEncodingError)
return
throw MultipeerCommunicatorError.communicatorMessageEncodingError
}
do {
try session.send(serializedMessage, toPeers: [peerID], with: .reliable)
completionHandler?(true, nil)
} catch {
completionHandler?(false, error)
print("Failed to send message \(error)")
}
}
}
Expand All @@ -97,13 +94,14 @@ extension MultipeerCommunicator: MCNearbyServiceBrowserDelegate {
print("found peer \(peerID.displayName)")
foundPeers[peerID] = info
let session = getSessionFor(peerID) ?? createNewSession(for: peerID)

browser.invitePeer(peerID, to: session, withContext: nil, timeout: MultipeerCommunicator.peerInvitationTimeout)
print("invited peer \(peerID.displayName)")
}

func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
print("lost \(peerID.displayName)")
if sessionsByPeerID.removeValue(forKey: peerID) != nil {
if connectedPeers.remove(peerID) != nil {
delegate?.didLoseUser(userID: peerID.displayName)
}
}
Expand Down Expand Up @@ -135,7 +133,11 @@ extension MultipeerCommunicator: MCSessionDelegate {
print("Couldn't decode message from \(peerID.displayName)")
return
}
delegate?.didReceiveMessage(text: message, fromUser: peerID.displayName, toUser: myPeerID.displayName)
if connectedPeers.contains(peerID) {
delegate?.didReceiveMessage(text: message, fromUser: peerID.displayName, toUser: myPeerID.displayName)
} else {
print("Received message from not connected peer ¯\\_(ツ)_/¯")
}
}

func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
Expand All @@ -145,12 +147,14 @@ extension MultipeerCommunicator: MCSessionDelegate {
if let peerDiscoveryInfo = foundPeers[peerID] {
print("connected with \(peerID.displayName)")
delegate?.didFindUser(userID: peerID.displayName, userName: peerDiscoveryInfo?[discoveryInfoUserNameKey])
connectedPeers.insert(peerID)
}
case .notConnected:
print("not connected with \(peerID.displayName)")
if sessionsByPeerID.removeValue(forKey: peerID) != nil {
if connectedPeers.remove(peerID) != nil {
delegate?.didLoseUser(userID: peerID.displayName)
}
sessionsByPeerID.removeValue(forKey: peerID)
foundPeers.removeValue(forKey: peerID)
session.disconnect()
case .connecting:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,8 @@ class CoreDataStack: ICoreDataStack {
}()

func performSave(in context: NSManagedObjectContext, completionHandler: (() -> Void)?) {
if context.hasChanges {
// No need for a capture list in the block below. As soon as the block finishes executing, context releases it.
// The block's retain count becomes zero and it is deallocated. When it deallocates, it releases 'self'.
context.perform {
context.perform {
if context.hasChanges {
do {
try context.save()
} catch {
Expand All @@ -57,9 +55,9 @@ class CoreDataStack: ICoreDataStack {
} else {
completionHandler?()
}
} else {
completionHandler?()
}
} else {
completionHandler?()
}
}

Expand Down Expand Up @@ -91,8 +89,7 @@ class CoreDataStack: ICoreDataStack {
configurationName: nil,
at: persistentStoreURL,
options: nil)
}
catch {
} catch {
fatalError("Unable to Load Persistent Store")
}
return persistentStoreCoordinator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,26 @@ extension AppUser {
private static let appUserDefaultID = UIDevice.current.identifierForVendor!.uuidString

static func findOrInsertAppUser(in context: NSManagedObjectContext) -> AppUser {
if let fetchedAppUser = fetchAppUser(in: context) {
return fetchedAppUser
} else {
let newAppUser = AppUser.insertAppUser(in: context)
return newAppUser
}
}

private static func fetchAppUser(in context: NSManagedObjectContext) -> AppUser? {
guard let model = context.persistentStoreCoordinator?.managedObjectModel else {
preconditionFailure("Model is not available in context")

}
var foundAppUser: AppUser?

let fetchRequest = AppUser.fetchRequestAppUser(model: model)
do {
let results = try context.fetch(fetchRequest)
assert(results.count < 2, "Multiple AppUsers found")
foundAppUser = results.first
return results.first
} catch {
print("Failed to fetch AppUser \(error)")
fatalError("Failed to fetch AppUser: \(error)")
}
return foundAppUser ?? AppUser.insertAppUser(in: context)
}

private static func insertAppUser(in context: NSManagedObjectContext) -> AppUser {
Expand Down
50 changes: 50 additions & 0 deletions TinkoffChat/Core Layer/Storage/MO Extensions/Conversation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// Conversation.swift
// TinkoffChat
//
// Created by Nikita Borodulin on 10/11/2017.
// Copyright © 2017 com.nikitaborodulin. All rights reserved.
//

import CoreData

extension Conversation {
private static let fetchRequestConversationByIDTemplateName = "ConversationByID"
private static let fetchRequestConversationByIDAttributeName = "conversationID"

static func findOrInsertConversationWith(id: String, in context: NSManagedObjectContext) -> Conversation {
if let fetchedConversation = fetchConversationWith(id: id, in: context) {
return fetchedConversation
} else {
let newConversation = Conversation.insertConversationWith(id: id, in: context)
return newConversation
}
}

private static func fetchConversationWith(id: String, in context: NSManagedObjectContext) -> Conversation? {
guard let model = context.persistentStoreCoordinator?.managedObjectModel else {
preconditionFailure("Model is not available in context")
}
let fetchRequest = Conversation.fetchRequestConversationBy(id: id, model: model)
do {
let results = try context.fetch(fetchRequest)
assert(results.count < 2, "Multiple Conversations found for id: \(id)")
return results.first
} catch {
fatalError("Failed to fetch User with id: \(id) \(error)")
}
}

private static func insertConversationWith(id: String, in context: NSManagedObjectContext) -> Conversation {
let newConversation = Conversation(context: context)
newConversation.conversationID = id
return newConversation
}

private static func fetchRequestConversationBy(id: String, model: NSManagedObjectModel) -> NSFetchRequest<Conversation> {
guard let fetchRequest = model.fetchRequestFromTemplate(withName: fetchRequestConversationByIDTemplateName, substitutionVariables: [fetchRequestConversationByIDAttributeName: id]) as? NSFetchRequest<Conversation> else {
preconditionFailure("No template with \(fetchRequestConversationByIDTemplateName) name")
}
return fetchRequest
}
}
24 changes: 24 additions & 0 deletions TinkoffChat/Core Layer/Storage/MO Extensions/Message.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Message.swift
// TinkoffChat
//
// Created by Nikita Borodulin on 12/11/2017.
// Copyright © 2017 com.nikitaborodulin. All rights reserved.
//

import CoreData

extension Message {
private static let fetchRequestMessageByIDTemplateName = "MessagesByConversationID"
private static let fetchRequestMessageByIDAttributeName = "id"

static func fetchRequestMessage(withConversationID conversationID: String, in context: NSManagedObjectContext) -> NSFetchRequest<Message> {
guard let model = context.persistentStoreCoordinator?.managedObjectModel else {
preconditionFailure("Model is not available in context")
}
guard let fetchRequest = model.fetchRequestFromTemplate(withName: fetchRequestMessageByIDTemplateName, substitutionVariables: [fetchRequestMessageByIDAttributeName: conversationID]) as? NSFetchRequest<Message> else {
preconditionFailure("No template with \(fetchRequestMessageByIDTemplateName) name")
}
return fetchRequest
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,29 @@ import CoreData

extension User {
private static let fetchRequestUserByIDTemplateName = "UserByID"
private static let fetchRequestUserByIDAttributeName = "id"
private static let fetchRequestUserByIDAttributeName = "userID"

static func findOrInsertUserWith(id: String, in context: NSManagedObjectContext) -> User? {
static func findOrInsertUserWith(id: String, in context: NSManagedObjectContext) -> User {
if let fetchedUser = fetchUserWith(id: id, in: context) {
return fetchedUser
} else {
let newUser = User.insertUserWith(id: id, in: context)
return newUser
}
}

private static func fetchUserWith(id: String, in context: NSManagedObjectContext) -> User? {
guard let model = context.persistentStoreCoordinator?.managedObjectModel else {
assertionFailure("Model is not available in context")
return nil
preconditionFailure("Model is not available in context")
}
var user: User?
let fetchRequest = User.fetchRequestUserBy(id: id, model: model)
do {
let results = try context.fetch(fetchRequest)
assert(results.count < 2, "Multiple AppUsers found")
if let foundUser = results.first {
user = foundUser
}
return results.first
} catch {
print("Failed to fetch User with id: \(id) \(error)")
fatalError("Failed to fetch User with id: \(id) \(error)")
}
if user == nil {
user = User.insertUserWith(id: id, in: context)
}
return user
}

private static func insertUserWith(id: String, in context: NSManagedObjectContext) -> User {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,41 @@
<entity name="AppUser" representedClassName="AppUser" syncable="YES" codeGenerationType="class">
<relationship name="currentUser" maxCount="1" deletionRule="Nullify" destinationEntity="User" inverseName="appUser" inverseEntity="User" syncable="YES"/>
</entity>
<entity name="Conversation" representedClassName="Conversation" syncable="YES" codeGenerationType="class">
<attribute name="conversationID" attributeType="String" syncable="YES"/>
<attribute name="isOnline" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="isUnread" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<relationship name="lastMessage" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Message" inverseName="lastMessageInConversation" inverseEntity="Message" syncable="YES"/>
<relationship name="messages" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Message" inverseName="conversation" inverseEntity="Message" syncable="YES"/>
<relationship name="participant" maxCount="1" deletionRule="Nullify" destinationEntity="User" inverseName="conversation" inverseEntity="User" syncable="YES"/>
</entity>
<entity name="Message" representedClassName="Message" syncable="YES" codeGenerationType="class">
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="isOutgoing" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="messageID" attributeType="String" syncable="YES"/>
<attribute name="text" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="conversation" maxCount="1" deletionRule="Nullify" destinationEntity="Conversation" inverseName="messages" inverseEntity="Conversation" syncable="YES"/>
<relationship name="lastMessageInConversation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Conversation" inverseName="lastMessage" inverseEntity="Conversation" syncable="YES"/>
</entity>
<entity name="User" representedClassName="User" syncable="YES" codeGenerationType="class">
<attribute name="info" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="isOnline" optional="YES" attributeType="Boolean" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="profileImage" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="userID" attributeType="String" syncable="YES"/>
<relationship name="appUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppUser" inverseName="currentUser" inverseEntity="AppUser" syncable="YES"/>
<fetchIndex name="byUserID">
<fetchIndexElement property="userID" type="Binary" order="ascending"/>
</fetchIndex>
<relationship name="conversation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Conversation" inverseName="participant" inverseEntity="Conversation" syncable="YES"/>
</entity>
<fetchRequest name="AppUser" entity="AppUser"/>
<fetchRequest name="UserByID" entity="User" predicateString="userID == &quot;$id&quot;"/>
<fetchRequest name="ConversationByID" entity="Conversation" predicateString="conversationID == $conversationID"/>
<fetchRequest name="ConversationsByOnlineUserID" entity="Conversation" predicateString="participant.isOnline == 1 AND messages != nil"/>
<fetchRequest name="MessagesByConversationID" entity="Message" predicateString="conversation.conversationID == $id"/>
<fetchRequest name="OnlineUsers" entity="User" predicateString="isOnline == 1"/>
<fetchRequest name="UserByID" entity="User" predicateString="userID == $userID"/>
<elements>
<element name="AppUser" positionX="-63" positionY="-18" width="128" height="60"/>
<element name="User" positionX="108" positionY="18" width="128" height="120"/>
<element name="AppUser" positionX="-72" positionY="39" width="128" height="60"/>
<element name="Conversation" positionX="-63" positionY="180" width="128" height="135"/>
<element name="Message" positionX="-243" positionY="176" width="128" height="133"/>
<element name="User" positionX="108" positionY="18" width="128" height="148"/>
</elements>
</model>
Loading

0 comments on commit 4ed8902

Please sign in to comment.