diff --git a/Secretly.xcodeproj/project.pbxproj b/Secretly.xcodeproj/project.pbxproj index 9084066..664f44f 100644 --- a/Secretly.xcodeproj/project.pbxproj +++ b/Secretly.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 3013628D269A91E50001580D /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3013628C269A91E50001580D /* KeychainError.swift */; }; + 301649F7268F967E00F26F4D /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301649F6268F967E00F26F4D /* KeychainStore.swift */; }; 302B5845267E658E007133E6 /* HttpResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B583D267E658E007133E6 /* HttpResponse.swift */; }; 302B5846267E658E007133E6 /* AmacaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B583E267E658E007133E6 /* AmacaConfig.swift */; }; 302B5847267E658E007133E6 /* StatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B583F267E658E007133E6 /* StatusCode.swift */; }; @@ -52,6 +54,7 @@ 30C77CB4266AF47300A888DC /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C77CB3266AF47300A888DC /* Credentials.swift */; }; 30C77CB6266AF48300A888DC /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C77CB5266AF48300A888DC /* CurrentUser.swift */; }; 30C77CB8266BD44300A888DC /* CreatePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C77CB7266BD44300A888DC /* CreatePostViewController.swift */; }; + 30FBE8602690E26D00557F41 /* KeychainStoreServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FBE85F2690E26D00557F41 /* KeychainStoreServiceTest.swift */; }; 30FD0E722659645A006E309A /* Faker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD0E712659645A006E309A /* Faker.swift */; }; E021984723FA35E00025C28E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E021984623FA35E00025C28E /* AppDelegate.swift */; }; E021984923FA35E00025C28E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E021984823FA35E00025C28E /* SceneDelegate.swift */; }; @@ -73,6 +76,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3013628C269A91E50001580D /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = ""; }; + 301649F6268F967E00F26F4D /* KeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = ""; }; 302B583D267E658E007133E6 /* HttpResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpResponse.swift; sourceTree = ""; }; 302B583E267E658E007133E6 /* AmacaConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmacaConfig.swift; sourceTree = ""; }; 302B583F267E658E007133E6 /* StatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusCode.swift; sourceTree = ""; }; @@ -118,6 +123,7 @@ 30C77CB3266AF47300A888DC /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; 30C77CB5266AF48300A888DC /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = ""; }; 30C77CB7266BD44300A888DC /* CreatePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePostViewController.swift; sourceTree = ""; }; + 30FBE85F2690E26D00557F41 /* KeychainStoreServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStoreServiceTest.swift; sourceTree = ""; }; 30FD0E712659645A006E309A /* Faker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Faker.swift; sourceTree = ""; }; E021984323FA35E00025C28E /* Secretly.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretly.app; sourceTree = BUILT_PRODUCTS_DIR; }; E021984623FA35E00025C28E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -207,9 +213,11 @@ isa = PBXGroup; children = ( 30BC8BA32662BDEF00F7E6A5 /* ImageStore.swift */, + 301649F6268F967E00F26F4D /* KeychainStore.swift */, 30BC8BA52662C02300F7E6A5 /* CacheImage.swift */, 30BC8BA12662BB0000F7E6A5 /* DataContainer.swift */, 30BC8B9F2662B8A700F7E6A5 /* StorageType.swift */, + 3013628C269A91E50001580D /* KeychainError.swift */, ); path = Storage; sourceTree = ""; @@ -282,6 +290,7 @@ children = ( E021985D23FA35E20025C28E /* SecretlyTests.swift */, E021985F23FA35E20025C28E /* Info.plist */, + 30FBE85F2690E26D00557F41 /* KeychainStoreServiceTest.swift */, ); path = SecretlyTests; sourceTree = ""; @@ -411,6 +420,7 @@ E021984B23FA35E00025C28E /* WelcomeViewController.swift in Sources */, 3072FBDF2680FA5A00B35C8C /* ImageProcessor.swift in Sources */, 302BB622267E38E800FD74F5 /* PostInputViewController+UIImagePickerControllerDelegate.swift in Sources */, + 3013628D269A91E50001580D /* KeychainError.swift in Sources */, 302B5845267E658E007133E6 /* HttpResponse.swift in Sources */, 302B584A267E658E007133E6 /* RestClient.swift in Sources */, 302B5848267E658E007133E6 /* HttpClient.swift in Sources */, @@ -430,6 +440,7 @@ 3033795D267537B40066D94A /* FeedCollectionViewController+UICollectionViewDelegateFlowLayout .swift in Sources */, 30BC8BA82662CEBA00F7E6A5 /* Checksum.swift in Sources */, 30FD0E722659645A006E309A /* Faker.swift in Sources */, + 301649F7268F967E00F26F4D /* KeychainStore.swift in Sources */, 30337957267536E30066D94A /* FeedCollectionViewController+UICollectionViewDelegate.swift in Sources */, 307A305E2661CD510020DF8B /* PostCollectionViewCell.swift in Sources */, 302BB626267E447900FD74F5 /* PostInputViewController+UITextFieldDelegate.swift in Sources */, @@ -461,6 +472,7 @@ buildActionMask = 2147483647; files = ( E021985E23FA35E20025C28E /* SecretlyTests.swift in Sources */, + 30FBE8602690E26D00557F41 /* KeychainStoreServiceTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Secretly/Base.lproj/Main.storyboard b/Secretly/Base.lproj/Main.storyboard index 4ed5b13..794742f 100644 --- a/Secretly/Base.lproj/Main.storyboard +++ b/Secretly/Base.lproj/Main.storyboard @@ -80,7 +80,7 @@ - + diff --git a/Secretly/Models/CurrentUser.swift b/Secretly/Models/CurrentUser.swift index fb28cfe..818bb98 100644 --- a/Secretly/Models/CurrentUser.swift +++ b/Secretly/Models/CurrentUser.swift @@ -10,7 +10,7 @@ import Foundation class CurrentUser { static func load() -> CurrentUser? { - guard let username = UserDefaults.standard.string(forKey: "secretly.username") else { + guard let username = try? KeychainStore.common.getItem(forKey: "secretly.username") else { return nil } return CurrentUser(username: username) @@ -20,7 +20,7 @@ class CurrentUser { init(username: String) { self.username = username - UserDefaults.standard.set(username, forKey: "secretly.username") + _ = KeychainStore.common.setItem(key: "secretly.username", value: username) } func credentials() -> Credentials { @@ -32,12 +32,12 @@ class CurrentUser { } private func password() -> String? { - return UserDefaults.standard.string(forKey: "secretly.password") + return try? KeychainStore.common.getItem(forKey: "secretly.password") } private func genPassword() -> String { let newPsswd = UUID().uuidString - UserDefaults.standard.set(newPsswd, forKey: "secretly.password") + _ = KeychainStore.common.setItem(key: "secretly.password", value: newPsswd) return newPsswd } } diff --git a/Secretly/Network/AmacaConfig.swift b/Secretly/Network/AmacaConfig.swift index 173da18..4a3dae9 100644 --- a/Secretly/Network/AmacaConfig.swift +++ b/Secretly/Network/AmacaConfig.swift @@ -11,7 +11,7 @@ import Foundation struct AmacaConfig { static let shared = AmacaConfig() var host: String { - values["host"] as! String + return values["host"] as! String } var httpClient: HttpClient { HttpClient(session: URLSession.shared, baseUrl: host) @@ -19,12 +19,13 @@ struct AmacaConfig { var apiToken: String? { get { - UserDefaults.standard.string(forKey: "amaca.apitoken") + try? KeychainStore.common.getItem(forKey: "amaca.apitoken") + } } func setApiToken(_ value: String) { - UserDefaults.standard.set(value, forKey: "amaca.apitoken") + _ = KeychainStore.common.setItem(key: "amaca.apitoken", value: value) } private var filepath: String { diff --git a/Secretly/Storage/KeychainError.swift b/Secretly/Storage/KeychainError.swift new file mode 100644 index 0000000..381c455 --- /dev/null +++ b/Secretly/Storage/KeychainError.swift @@ -0,0 +1,15 @@ +// +// KeychainError.swift +// Secretly +// +// Created by Luis Abraham Ortega Gonzalez on 10/07/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation + +enum KeychainError: Error { + case noItem + case unexpectedItemData + case unhandledError(status: OSStatus) +} diff --git a/Secretly/Storage/KeychainStore.swift b/Secretly/Storage/KeychainStore.swift new file mode 100644 index 0000000..8a9198a --- /dev/null +++ b/Secretly/Storage/KeychainStore.swift @@ -0,0 +1,133 @@ +// +// KeychainService.swift +// Secretly +// +// Created by Luis Abraham Ortega Gonzalez on 02/07/21. +// Copyright © 2021 3zcurdia. All rights reserved. +// + +import Foundation + + +struct KeychainStore{ + + public let serviceName:String? + + private static let defaultServiceName: String = { + return Bundle.main.bundleIdentifier ?? "com.secretly.ioslab" + }() + + public static let common = KeychainStore() + + public init(serviceName:String? = nil){ + self.serviceName = serviceName + } + + private init(){ + self.init(serviceName: KeychainStore.defaultServiceName) + } + + + + public func setItem(key :String, value: String, accesibility: CFString? = nil) -> Bool { + let encodedValue = value.data(using: String.Encoding.utf8)! + return self.set(key: key, value: encodedValue, accesibility: accesibility) + } + + + private func set(key :String, value: Data, accesibility: CFString? = nil) -> Bool{ + var query = self.setUpKeychainDic(key: key,accesibility: accesibility) + + query[kSecValueData as String] = value + + let status = SecItemAdd(query as CFDictionary, nil) + if status == errSecSuccess { + return true + } else if status == errSecDuplicateItem { + return self.updateItem(key: key, value: value, accesibility: accesibility) + } else { + return false + } + } + + private func updateItem(key:String, value:Data, accesibility: CFString? = nil) -> Bool { + + let query = self.setUpKeychainDic(key: key, accesibility: accesibility) + let update:[String:Any] = [kSecValueData as String:value] + + let status = SecItemUpdate(query as CFDictionary, update as CFDictionary) + + if status == errSecSuccess { + return true + } else { + return false + } + } + + + public func getItem(forKey key: String) throws -> String{ + var query = self.setUpKeychainDic(key: key) + query[kSecReturnAttributes as String] = true + query[kSecReturnData as String] = true + query[kSecMatchLimit as String] = kSecMatchLimitOne + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + + guard status != errSecItemNotFound else { throw KeychainError.noItem } + guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } + + guard let existingItem = item as? [String : Any], + let itemData = existingItem[kSecValueData as String] as? Data, + let stringData = String(data: itemData, encoding: String.Encoding.utf8) + else { + throw KeychainError.unexpectedItemData + } + + return stringData + } + + + public func deleteItem(forKey key: String, accesibility: CFString? = nil) -> Bool{ + let query = self.setUpKeychainDic(key: key, accesibility: accesibility) + let status: OSStatus = SecItemDelete(query as CFDictionary) + if status == errSecSuccess { + return true + } else { + return false + } + + } + + public func deleteAllItems() -> Bool { + var query = [String:Any]() + query[kSecClass as String] = kSecClassInternetPassword + query[kSecAttrServer as String] = self.serviceName ?? KeychainStore.defaultServiceName + + let status: OSStatus = SecItemDelete(query as CFDictionary) + if status == errSecSuccess { + return true + } else { + return false + } + } + + private func setUpKeychainDic(key: String, accesibility: CFString? = nil) -> [String:Any]{ + var keychainQueryDictionary: [String:Any] = [String: Any]() + // Uniquely identify this keychain accessor + let encodedIdentifier: Data? = key.data(using: String.Encoding.utf8) + keychainQueryDictionary[kSecClass as String] = kSecClassInternetPassword + keychainQueryDictionary[kSecAttrDescription as String] = encodedIdentifier + keychainQueryDictionary[kSecAttrAccount as String] = encodedIdentifier + keychainQueryDictionary[kSecAttrServer as String] = self.serviceName ?? KeychainStore.defaultServiceName + + if let accessString = accesibility { + keychainQueryDictionary[kSecAttrAccessible as String] = accessString + } + + + return keychainQueryDictionary + } + +} diff --git a/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDataSource .swift b/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDataSource .swift index 15100dd..98fa139 100644 --- a/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDataSource .swift +++ b/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDataSource .swift @@ -23,4 +23,5 @@ extension FeedCollectionViewController: UICollectionViewDataSource { cell.post = self.posts?[indexPath.row] return cell } + } diff --git a/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDelegateFlowLayout .swift b/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDelegateFlowLayout .swift index e6c7ccc..b841a37 100644 --- a/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDelegateFlowLayout .swift +++ b/Secretly/ViewControllers/FeedCollectionViewController+UICollectionViewDelegateFlowLayout .swift @@ -9,7 +9,15 @@ import UIKit extension FeedCollectionViewController: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return CGSize(width: collectionView.bounds.width, height: 300) + let numberOfItemsPerRow:CGFloat = 1 + let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout + + let totalSpacing = Int(flowLayout.sectionInset.left) + Int(flowLayout.sectionInset.right) + Int((numberOfItemsPerRow-1) * flowLayout.minimumInteritemSpacing) + let width = (Int(collectionView.bounds.width) - totalSpacing)/Int(numberOfItemsPerRow) + return CGSize(width: width, height: 300) } + } diff --git a/Secretly/ViewControllers/FeedCollectionViewController.swift b/Secretly/ViewControllers/FeedCollectionViewController.swift index 9180d42..b6173f7 100644 --- a/Secretly/ViewControllers/FeedCollectionViewController.swift +++ b/Secretly/ViewControllers/FeedCollectionViewController.swift @@ -30,14 +30,19 @@ class FeedCollectionViewController: UIViewController { } func setupCollectionView() { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 4 + layout.minimumInteritemSpacing = 4 + layout.sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) postInputView.delegate = self collectionView.delegate = self collectionView.dataSource = self collectionView.prefetchDataSource = self +// collectionView.isPrefetchingEnabled = true + collectionView.collectionViewLayout = layout let nib = UINib(nibName: String(describing: PostCollectionViewCell.self), bundle: nil) collectionView.register(nib, forCellWithReuseIdentifier: PostCollectionViewCell.reuseIdentifier) collectionView.addSubview(refreshControl) - refreshControl.addTarget(self, action: #selector(self.loadPosts), for: UIControl.Event.valueChanged) } diff --git a/Secretly/Views/PostCollectionViewCell.xib b/Secretly/Views/PostCollectionViewCell.xib index 7b3a4e2..513f15e 100644 --- a/Secretly/Views/PostCollectionViewCell.xib +++ b/Secretly/Views/PostCollectionViewCell.xib @@ -23,6 +23,12 @@ + + + + + +