├── PersonalLogs ├── Assets.xcassets │ ├── Contents.json │ ├── logo.imageset │ │ ├── logo.png │ │ ├── logoDarkMode.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon-20.png │ │ ├── icon-29.png │ │ ├── icon-40.png │ │ ├── icon-76.png │ │ ├── icon-1024.png │ │ ├── icon-20@2x.png │ │ ├── icon-20@3x.png │ │ ├── icon-29@2x.png │ │ ├── icon-29@3x.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-76@2x.png │ │ ├── icon-83.5@2x.png │ │ └── Contents.json │ ├── emptyState.imageset │ │ ├── emptyState.png │ │ ├── emptyStateDarkMode.png │ │ └── Contents.json │ ├── textColor.colorset │ │ └── Contents.json │ ├── primaryActionColor.colorset │ │ └── Contents.json │ ├── secundaryActionColor.colorset │ │ └── Contents.json │ ├── backgroundColor.colorset │ │ └── Contents.json │ └── backgroundCellColor.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Protocols │ ├── NoteDelegate.swift │ ├── NoteListDelegate.swift │ ├── RepositoryItem.swift │ └── Repository.swift ├── Model │ ├── NoteRepository.swift │ ├── OnBoardingStatus.swift │ └── Note.swift ├── Extensions │ ├── ColorExtension.swift │ └── FileManagerExtension.swift ├── ViewControllers │ ├── OnBoardingViewController.swift │ ├── NoteViewController.swift │ └── ListViewController.swift ├── Helpers │ ├── Keychain │ │ ├── KeychainHelper.swift │ │ └── KeychainPasswordItem.swift │ └── FileHelper.swift ├── AppDelegate.swift ├── Info.plist ├── SceneDelegate.swift ├── Base.lproj │ └── LaunchScreen.storyboard └── View │ ├── OnBoardingView.swift │ ├── NoteView.swift │ └── ListView.swift ├── PersonalLogsTests ├── Info.plist └── PersonalLogsTests.swift ├── LICENSE ├── .gitignore ├── README.md └── PersonalLogs.xcodeproj └── project.pbxproj /PersonalLogs/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PersonalLogs/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/logo.imageset/logo.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-20.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/logo.imageset/logoDarkMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/logo.imageset/logoDarkMode.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/emptyState.imageset/emptyState.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/emptyState.imageset/emptyState.png -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/emptyState.imageset/emptyStateDarkMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielaBezerra/PersonalLogs/HEAD/PersonalLogs/Assets.xcassets/emptyState.imageset/emptyStateDarkMode.png -------------------------------------------------------------------------------- /PersonalLogs/Protocols/NoteDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteDelegate.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 26/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol NoteDelegate: AnyObject { 12 | func didChange(title: String, body: String) 13 | } 14 | -------------------------------------------------------------------------------- /PersonalLogs/Model/NoteRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesRepository.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class NoteRepository: Repository { 12 | typealias Item = Note 13 | var items: [Note] = [] 14 | } 15 | -------------------------------------------------------------------------------- /PersonalLogs/Protocols/NoteListDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteListDelegate.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 26/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol NoteListDelegate: AnyObject { 12 | func didSelectNote(with id: UUID) 13 | func deleteNote(for id: UUID) 14 | func askForPasswordToLock(for id: UUID) 15 | func askForPasswordToUnlock(for id: UUID) 16 | } 17 | -------------------------------------------------------------------------------- /PersonalLogs/Model/OnBoardingStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnBoardingStatus.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 01/06/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - Codable 12 | struct OnBoardingStatus: Codable { 13 | static let plistName: String = "OnBoardingStatus" 14 | 15 | let firstLaunch: Bool = true 16 | let firstLaunchTimestamp: TimeInterval = Date().timeIntervalSince1970 17 | 18 | private enum CodingKeys: String, CodingKey { 19 | case firstLaunch, firstLaunchTimestamp 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PersonalLogs/Extensions/ColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorExtensions.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | static let primaryAction: UIColor = UIColor(named: "primaryActionColor")! 13 | static let secundaryAction: UIColor = UIColor(named: "secundaryActionColor")! 14 | static let text: UIColor = UIColor(named: "textColor")! 15 | static let background: UIColor = UIColor(named: "backgroundColor")! 16 | static let backgroundCell: UIColor = UIColor(named: "backgroundCellColor")! 17 | } 18 | -------------------------------------------------------------------------------- /PersonalLogs/ViewControllers/OnBoardingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnBoardingViewController.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 01/06/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class OnBoardingViewController: UIViewController { 13 | 14 | lazy var onBoardingView: OnBoardingView = { 15 | let view = OnBoardingView() 16 | view.dismissAction = { 17 | self.dismiss(animated: true, completion: nil) 18 | } 19 | return view 20 | }() 21 | 22 | override func loadView() { 23 | self.view = onBoardingView 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /PersonalLogsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/textColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.090", 9 | "green" : "0.169", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.874", 27 | "green" : "1.000", 28 | "red" : "0.916" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/primaryActionColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.349", 9 | "green" : "0.627", 10 | "red" : "0.435" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.391", 27 | "green" : "0.742", 28 | "red" : "0.503" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/secundaryActionColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.231", 9 | "green" : "0.141", 10 | "red" : "0.729" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.252", 27 | "green" : "0.147", 28 | "red" : "0.849" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/backgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.800", 9 | "green" : "1.000", 10 | "red" : "0.880" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.082", 27 | "green" : "0.138", 28 | "red" : "0.104" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/backgroundCellColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.581", 9 | "green" : "0.900", 10 | "red" : "0.707" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.148", 27 | "green" : "0.253", 28 | "red" : "0.189" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gabriela Bezerra 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 | -------------------------------------------------------------------------------- /PersonalLogsTests/PersonalLogsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersonalLogsTests.swift 3 | // PersonalLogsTests 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PersonalLogs 11 | 12 | class PersonalLogsTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() throws { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() throws { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "logoDarkMode.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PersonalLogs/Protocols/RepositoryItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryItem.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 01/06/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - RepositoryItem 12 | protocol RepositoryItem: AnyObject, Codable { 13 | var id: UUID { get } 14 | var locked: Bool { get set } 15 | func lock(password: String) -> Bool 16 | func unlock(password: String) -> Bool 17 | init() 18 | } 19 | 20 | //MARK: - RepositoryItem Default Implementations 21 | extension RepositoryItem { 22 | 23 | func lock(password: String) -> Bool { 24 | //save password on keychain 25 | let success = KeychainHelper().save(password: password, id: self.id) 26 | 27 | self.locked = success 28 | return success 29 | } 30 | 31 | func unlock(password: String) -> Bool { 32 | //check if password matches with the one on keychain 33 | let success = KeychainHelper().check(password: password, for: self.id) 34 | 35 | self.locked = success ? false : true 36 | return success 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/emptyState.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "emptyState.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "emptyStateDarkMode.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PersonalLogs/Model/Note.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Note.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - Codable 12 | class Note: RepositoryItem { 13 | 14 | static let placeholder: (title: String, body: String) = (title: "Untitled Note", 15 | body: "It's your story to tell.") 16 | 17 | required init() { 18 | self.id = UUID() 19 | self.title = Self.placeholder.title 20 | self._body = Self.placeholder.body 21 | self.locked = false 22 | } 23 | 24 | let id: UUID 25 | var title: String 26 | var locked: Bool 27 | private var _body: String 28 | var body: String { 29 | get { locked ? "Classified. A secret is a secret, you know." : _body } 30 | set { _body = locked ? _body : newValue } 31 | } 32 | 33 | //Enum defining which properties are going to be saved. The "_body" property is going to be saved as "body" 34 | private enum CodingKeys: String, CodingKey { 35 | case id, title, _body = "body", locked 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /PersonalLogs/Helpers/Keychain/KeychainHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainHelper.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 01/06/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct KeychainHelper { 12 | 13 | private let service = "PersonalLogsService" 14 | 15 | func check(password: String, for id: UUID) -> Bool { 16 | do { 17 | let item = KeychainPasswordItem(service: service, account: id.uuidString) 18 | let keychainPassword = try item.readPassword() 19 | if keychainPassword == password { try item.deleteItem() } 20 | return keychainPassword == password 21 | } catch { 22 | print(error) 23 | return false 24 | } 25 | } 26 | 27 | func save(password: String, id: UUID) -> Bool { 28 | do { 29 | try KeychainPasswordItem(service: service, account: id.uuidString).savePassword(password) 30 | return true 31 | } catch { 32 | print(error) 33 | return false 34 | } 35 | } 36 | 37 | @discardableResult 38 | func removePassword(from id: UUID) -> Bool { 39 | do { 40 | try KeychainPasswordItem(service: service, account: id.uuidString).deleteItem() 41 | return true 42 | } catch { 43 | print(error) 44 | return false 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /PersonalLogs/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /PersonalLogs/Extensions/FileManagerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManagerExtension.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 5/31/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension FileManager { 13 | 14 | func printContent(from path: String, recursivelly: Bool = false) { 15 | let lastComponent = path.split(separator: "/").last! 16 | var isDirectory: ObjCBool = false 17 | fileExists(atPath: path, isDirectory: &isDirectory) 18 | if isDirectory.boolValue { 19 | print("\n📁 Directory: \(lastComponent)") 20 | if let contents = try? contentsOfDirectory(atPath: path) { 21 | print("Contents: \(contents)") 22 | if recursivelly { 23 | for item in contents { 24 | let itemPath = path+"/"+(item.replacingOccurrences(of: " ", with: "\\ ")) 25 | printContent(from: itemPath, recursivelly: true) 26 | } 27 | } 28 | } 29 | } else { 30 | print("\n📊 File: \(lastComponent)") 31 | let fileExtension = lastComponent.split(separator: ".").last 32 | switch fileExtension { 33 | case "plist": 34 | let url = URL(fileURLWithPath: path) 35 | if let data = try? Data(contentsOf: url), 36 | let plist = try? PropertyListSerialization.propertyList(from: data,options: .mutableContainers, format: nil) as? [String : Any] { 37 | print("Content: \(plist.description)") 38 | } 39 | default: 40 | if let content = try? String(contentsOfFile: path, encoding: .utf8) { 41 | print("Content: \(content)") 42 | } 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /PersonalLogs/ViewControllers/NoteViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteViewController.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NoteViewController: UIViewController { 12 | 13 | private let noteRepository: NoteRepository 14 | private let noteID: UUID 15 | private var note: Note? { 16 | noteRepository.readItem(id: noteID) 17 | } 18 | 19 | lazy var noteView: NoteView = { 20 | let view = NoteView() 21 | view.delegate = self 22 | return view 23 | }() 24 | 25 | init(noteRepository: NoteRepository, id: UUID) { 26 | self.noteRepository = noteRepository 27 | self.noteID = id 28 | super.init(nibName: nil, bundle: nil) 29 | } 30 | 31 | override func loadView() { 32 | self.view = noteView 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | setupNavigationItem() 38 | 39 | noteView.titleField.text = note?.title 40 | noteView.bodyTextView.text = note?.body 41 | } 42 | 43 | func setupNavigationItem() { 44 | navigationItem.largeTitleDisplayMode = .never 45 | navigationController?.navigationBar.standardAppearance.configureWithTransparentBackground() 46 | navigationController?.navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | } 54 | 55 | 56 | extension NoteViewController: NoteDelegate { 57 | 58 | func didChange(title: String, body: String) { 59 | guard let note = note else { return } 60 | note.title = title 61 | note.body = body 62 | noteRepository.update(item: note) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /PersonalLogs/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UIStatusBarTintParameters 47 | 48 | UINavigationBar 49 | 50 | Style 51 | UIBarStyleDefault 52 | Translucent 53 | 54 | 55 | 56 | UISupportedInterfaceOrientations 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationLandscapeLeft 60 | UIInterfaceOrientationLandscapeRight 61 | 62 | UISupportedInterfaceOrientations~ipad 63 | 64 | UIInterfaceOrientationPortrait 65 | UIInterfaceOrientationPortraitUpsideDown 66 | UIInterfaceOrientationLandscapeLeft 67 | UIInterfaceOrientationLandscapeRight 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /PersonalLogs/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | lazy var navigationController: UINavigationController = { 17 | let navigationController = UINavigationController(rootViewController: ListViewController()) 18 | navigationController.navigationBar.prefersLargeTitles = true 19 | navigationController.navigationBar.tintColor = .primaryAction 20 | return navigationController 21 | }() 22 | 23 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 24 | 25 | if let windowScene = scene as? UIWindowScene { 26 | let window = UIWindow(windowScene: windowScene) 27 | window.rootViewController = navigationController 28 | self.window = window 29 | window.makeKeyAndVisible() 30 | 31 | //MARK: - App Sandbox 32 | // FileManager.default.printContent(from: NSHomeDirectory(), recursivelly: true) 33 | 34 | //MARK: - PropertyLists 35 | // let onBoardingStatusURL = URL(fileURLWithPath: NSHomeDirectory()+"/Library/Preferences/"+OnBoardingStatus.plistName) 36 | // 37 | // if let data = try? Data(contentsOf: onBoardingStatusURL), 38 | // let onBoardingPropertyList = try? PropertyListDecoder().decode(OnBoardingStatus.self, from: data) { 39 | // 40 | // print("FirstLaunch:",onBoardingPropertyList.firstLaunch) 41 | // print("FirstLaunchTimestamp:",onBoardingPropertyList.firstLaunchTimestamp) 42 | // print("OnBoarding has already been seen by the user.") 43 | // 44 | // } else { 45 | // 46 | // let status = OnBoardingStatus() 47 | // if let data = try? PropertyListEncoder().encode(status) { 48 | // try? data.write(to: onBoardingStatusURL) 49 | // } 50 | // 51 | // navigationController.present(OnBoardingViewController(), animated: true) 52 | // } 53 | 54 | //MARK: - UserDefaults 55 | let isFirstLaunch = (UserDefaults.standard.value(forKey: "FirstLaunch") as? Bool) ?? false 56 | if !isFirstLaunch { 57 | UserDefaults.standard.set(true, forKey: "FirstLaunch") 58 | UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "FirstLaunchTimestamp") 59 | navigationController.present(OnBoardingViewController(), animated: true) 60 | } 61 | } 62 | 63 | } 64 | 65 | } 66 | 67 | -------------------------------------------------------------------------------- /PersonalLogs/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon-20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon-29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon-29@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon-29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "icon-40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon-40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "icon-60@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "icon-60@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "icon-20.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "2x", 66 | "size" : "20x20" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "1x", 71 | "size" : "29x29" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "29x29" 77 | }, 78 | { 79 | "filename" : "icon-40.png", 80 | "idiom" : "ipad", 81 | "scale" : "1x", 82 | "size" : "40x40" 83 | }, 84 | { 85 | "idiom" : "ipad", 86 | "scale" : "2x", 87 | "size" : "40x40" 88 | }, 89 | { 90 | "filename" : "icon-76.png", 91 | "idiom" : "ipad", 92 | "scale" : "1x", 93 | "size" : "76x76" 94 | }, 95 | { 96 | "filename" : "icon-76@2x.png", 97 | "idiom" : "ipad", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "icon-83.5@2x.png", 103 | "idiom" : "ipad", 104 | "scale" : "2x", 105 | "size" : "83.5x83.5" 106 | }, 107 | { 108 | "filename" : "icon-1024.png", 109 | "idiom" : "ios-marketing", 110 | "scale" : "1x", 111 | "size" : "1024x1024" 112 | } 113 | ], 114 | "info" : { 115 | "author" : "xcode", 116 | "version" : 1 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /PersonalLogs/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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/xcode,macos,swift 3 | # Edit at https://www.gitignore.io/?templates=xcode,macos,swift 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Swift ### 34 | # Xcode 35 | # 36 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 37 | 38 | ## Build generated 39 | build/ 40 | DerivedData/ 41 | 42 | ## Various settings 43 | *.pbxuser 44 | !default.pbxuser 45 | *.mode1v3 46 | !default.mode1v3 47 | *.mode2v3 48 | !default.mode2v3 49 | *.perspectivev3 50 | !default.perspectivev3 51 | xcuserdata/ 52 | 53 | ## Other 54 | *.moved-aside 55 | *.xccheckout 56 | *.xcscmblueprint 57 | 58 | ## Obj-C/Swift specific 59 | *.hmap 60 | *.ipa 61 | *.dSYM.zip 62 | *.dSYM 63 | 64 | ## Playgrounds 65 | timeline.xctimeline 66 | playground.xcworkspace 67 | 68 | # Swift Package Manager 69 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 70 | # Packages/ 71 | # Package.pins 72 | # Package.resolved 73 | .build/ 74 | # Add this line if you want to avoid checking in Xcode SPM integration. 75 | # .swiftpm/xcode 76 | 77 | # CocoaPods 78 | # We recommend against adding the Pods directory to your .gitignore. However 79 | # you should judge for yourself, the pros and cons are mentioned at: 80 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 81 | # Pods/ 82 | # Add this line if you want to avoid checking in source code from the Xcode workspace 83 | # *.xcworkspace 84 | 85 | # Carthage 86 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 87 | # Carthage/Checkouts 88 | 89 | Carthage/Build 90 | 91 | # Accio dependency management 92 | Dependencies/ 93 | .accio/ 94 | 95 | # fastlane 96 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 97 | # screenshots whenever they are needed. 98 | # For more information about the recommended setup visit: 99 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 100 | 101 | fastlane/report.xml 102 | fastlane/Preview.html 103 | fastlane/screenshots/**/*.png 104 | fastlane/test_output 105 | 106 | # Code Injection 107 | # After new code Injection tools there's a generated folder /iOSInjectionProject 108 | # https://github.com/johnno1962/injectionforxcode 109 | 110 | iOSInjectionProject/ 111 | 112 | ### Xcode ### 113 | # Xcode 114 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 115 | 116 | ## User settings 117 | 118 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 119 | 120 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 121 | 122 | ## Xcode Patch 123 | *.xcodeproj/* 124 | !*.xcodeproj/project.pbxproj 125 | !*.xcodeproj/xcshareddata/ 126 | !*.xcworkspace/contents.xcworkspacedata 127 | /*.gcno 128 | 129 | ### Xcode Patch ### 130 | **/xcshareddata/WorkspaceSettings.xcsettings 131 | 132 | # End of https://www.gitignore.io/api/xcode,macos,swift 133 | -------------------------------------------------------------------------------- /PersonalLogs/Protocols/Repository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repository.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 26/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - Repository 12 | protocol Repository: AnyObject { 13 | 14 | associatedtype Item: RepositoryItem 15 | 16 | var items: [Item] { get set } 17 | 18 | func createNewItem() -> Item? 19 | func readAllItems() -> [Item] 20 | func readItem(id: UUID) -> Item? 21 | func update(item: Item) 22 | func delete(id: UUID) 23 | func lock(id: UUID, password: String) -> Bool 24 | func unlock(id: UUID, password: String) -> Bool 25 | 26 | } 27 | 28 | //MARK: - Repository Default Implementations 29 | extension Repository { 30 | 31 | func createNewItem() -> Item? { 32 | 33 | //creating new item 34 | let newItem = Item() 35 | 36 | //persist file 37 | if let data = try? JSONEncoder().encode(newItem) { 38 | FileHelper().createFile(with: data, name: newItem.id.uuidString) 39 | return newItem 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func readAllItems() -> [Item] { 46 | 47 | //read the content of the documents path 48 | let fileNames: [String] = FileHelper().contentsForDirectory(atPath: "") 49 | 50 | //retrieve items from fileNames and updating items array 51 | self.items = fileNames.compactMap { fileName in 52 | if let data = FileHelper().retrieveFile(at: fileName) { 53 | //decode from Data type to Item type 54 | let item = try? JSONDecoder().decode(Item.self, from: data) 55 | // TODO: remove body content from here in case its locked 56 | return item 57 | } 58 | return nil 59 | } 60 | 61 | return items 62 | } 63 | 64 | func readItem(id: UUID) -> Item? { 65 | //read one file by name. In our case, note files are named with their id.uuidString. 66 | if let data = FileHelper().retrieveFile(at: id.uuidString) { 67 | //decode from Data type to Item type 68 | let item = try? JSONDecoder().decode(Item.self, from: data) 69 | return item 70 | } 71 | return nil 72 | } 73 | 74 | func update(item: Item) { 75 | //encode to Data format 76 | if let data = try? JSONEncoder().encode(item) { 77 | //overrite persisted file 78 | FileHelper().updateFile(at: item.id.uuidString, data: data) 79 | } 80 | } 81 | 82 | func delete(id: UUID) { 83 | //remove file 84 | FileHelper().removeFile(at: id.uuidString) 85 | //remove password from keychain 86 | KeychainHelper().removePassword(from: id) 87 | } 88 | 89 | func lock(id: UUID, password: String) -> Bool { 90 | //get item by id 91 | guard let item = readItem(id: id) else { return false } 92 | 93 | //lock item with password 94 | if item.lock(password: password) { 95 | //if it was successfully locked, persist its new locked state. 96 | update(item: item) 97 | return true 98 | } 99 | return false 100 | } 101 | 102 | func unlock(id: UUID, password: String) -> Bool { 103 | //get item by id 104 | guard let item = readItem(id: id) else { return false } 105 | 106 | //unlock item with password 107 | if item.unlock(password: password) { 108 | //if it was successfully unlocked, persist its new unlocked state. 109 | update(item: item) 110 | return true 111 | } 112 | return false 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /PersonalLogs/Helpers/FileHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileHelper.swift 3 | // FoundPersBas 4 | // 5 | // Created by Yuri on 04/04/19. 6 | // Copyright © 2019 academy.IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - FileManager 12 | struct FileHelper { 13 | 14 | let manager = FileManager.default 15 | let mainPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 16 | 17 | //Create a new directory on the documents by default, or on the choosen path 18 | func createDirectory(with name: String, at path: String? = nil) { 19 | let contentPath = constructPath(named: name, from: path) 20 | if !directoryExists(with: name, at: path) { 21 | do { 22 | try manager.createDirectory(at: contentPath, withIntermediateDirectories: true, attributes: nil) 23 | } catch (let error) { print(error.localizedDescription) } 24 | } 25 | } 26 | 27 | //Remove directory and all of it's contents 28 | func removeDirectory(named: String, at path: String? = nil) -> Bool { 29 | let dirPath = constructPath(named: named, from: path) 30 | do { 31 | try manager.removeItem(at: dirPath) 32 | return !manager.fileExists(atPath: dirPath.path) 33 | } catch (let error) { 34 | print(error.localizedDescription) 35 | return false 36 | } 37 | } 38 | 39 | //Check if the directory exists 40 | func directoryExists(with name: String, at path: String? = nil) -> Bool { 41 | let dirPath = constructPath(named: name, from: path) 42 | var isDirectory = ObjCBool(true) 43 | return manager.fileExists(atPath: dirPath.path, isDirectory: &isDirectory) && isDirectory.boolValue 44 | 45 | } 46 | 47 | func contentsForDirectory(atPath path: String) -> [String] { 48 | let contentPath = constructPath(named: path) 49 | let itens = try? manager.contentsOfDirectory(atPath: contentPath.path) 50 | return itens ?? [] 51 | } 52 | 53 | func fullPathForContents(at directory: String) -> [String] { 54 | let contentPath = constructPath(named: directory) 55 | guard let itens = try? manager.contentsOfDirectory(atPath: contentPath.path) else { 56 | return [] 57 | } 58 | 59 | return itens.map { "\(contentPath.path)/\($0)"} 60 | } 61 | 62 | @discardableResult 63 | func createFile(with data: Data, name: String) -> Bool { 64 | let contentPath = constructPath(named: name) 65 | manager.createFile(atPath: contentPath.path, contents: data, attributes: nil) 66 | return manager.fileExists(atPath: contentPath.path) 67 | } 68 | 69 | @discardableResult 70 | func removeFile(at path: String) -> Bool { 71 | let contentPath = constructPath(named: path) 72 | do { 73 | try manager.removeItem(at: contentPath) 74 | return !manager.fileExists(atPath: contentPath.path) 75 | } catch (let error) { 76 | print(error.localizedDescription) 77 | return false 78 | } 79 | } 80 | 81 | @discardableResult 82 | func updateFile(at path: String, data: Data) -> Bool { 83 | let contentPath = constructPath(named: path) 84 | do { 85 | try data.write(to: contentPath) 86 | return true 87 | } catch (let error) { 88 | print(error.localizedDescription) 89 | return false 90 | } 91 | } 92 | 93 | 94 | func retrieveFile(at path: String) -> Data? { 95 | let contentPath = constructPath(named: path) 96 | let data = try? Data(contentsOf: contentPath) 97 | return data 98 | } 99 | 100 | func constructPath(named: String, from path: String? = nil) -> URL { 101 | let contentPath = mainPath 102 | if let path = path { 103 | return contentPath.appendingPathComponent(path).appendingPathComponent(named) 104 | } else { 105 | return contentPath.appendingPathComponent(named) 106 | } 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /PersonalLogs/View/OnBoardingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnBoardingView.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 01/06/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class OnBoardingView: UIView { 13 | 14 | lazy var titleLabel: UILabel = { 15 | let label = UILabel() 16 | label.font = UIFont.systemFont(ofSize: 32, weight: .medium) 17 | label.textColor = .text 18 | label.numberOfLines = 0 19 | label.text = "Simple & Safe" 20 | return label 21 | }() 22 | 23 | lazy var bodyLabel: UILabel = { 24 | let label = UILabel() 25 | label.font = UIFont.systemFont(ofSize: 18, weight: .regular) 26 | label.textColor = .text 27 | label.numberOfLines = 0 28 | label.text = """ 29 | 30 | 📗 Personal Logs is the best way to save your personal notes and reflections. 31 | 32 | 1. Create a new note taping the + button on the right corner and write your note, quite simple. 33 | 34 | 2. Don't worry about saving the changes you make on your notes, every letter you type is automatically saved by default. 35 | 36 | 3. You can add a password for each individual note, if you'd like. Just find the note you want to lock on the Personal Logs list, and swipe right to see the option. 37 | 38 | ✨ Enjoy the app, and keep writing! 39 | 40 | 🖋 41 | """ 42 | return label 43 | }() 44 | 45 | lazy var startButton: UIButton = { 46 | let button = UIButton() 47 | button.setTitle("START WRITING", for: .normal) 48 | button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20) 49 | button.setTitleColor(.white, for: .normal) 50 | button.backgroundColor = .primaryAction 51 | button.addTarget(self, action: #selector(dismiss), for: .touchUpInside) 52 | return button 53 | }() 54 | 55 | var dismissAction: (() -> Void)! 56 | @objc func dismiss() { 57 | dismissAction() 58 | } 59 | 60 | override init(frame: CGRect) { 61 | super.init(frame: frame) 62 | self.backgroundColor = .background 63 | setupConstraints() 64 | } 65 | 66 | required init?(coder: NSCoder) { 67 | fatalError("init(coder:) has not been implemented") 68 | } 69 | 70 | override func layoutSubviews() { 71 | super.layoutSubviews() 72 | startButton.layer.cornerCurve = .continuous 73 | startButton.layer.cornerRadius = 11 74 | } 75 | 76 | func setupConstraints() { 77 | 78 | self.addSubview(titleLabel) 79 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 80 | NSLayoutConstraint.activate([ 81 | titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor), 82 | titleLabel.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 40), 83 | titleLabel.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.75) 84 | ]) 85 | 86 | self.addSubview(bodyLabel) 87 | bodyLabel.translatesAutoresizingMaskIntoConstraints = false 88 | NSLayoutConstraint.activate([ 89 | bodyLabel.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor), 90 | bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20), 91 | bodyLabel.widthAnchor.constraint(equalTo: titleLabel.widthAnchor) 92 | ]) 93 | 94 | self.addSubview(startButton) 95 | startButton.translatesAutoresizingMaskIntoConstraints = false 96 | NSLayoutConstraint.activate([ 97 | startButton.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor), 98 | startButton.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: -90), 99 | startButton.widthAnchor.constraint(equalTo: bodyLabel.widthAnchor, multiplier: 1), 100 | startButton.heightAnchor.constraint(equalToConstant: 50) 101 | ]) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /PersonalLogs/View/NoteView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteView.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NoteView: UIView { 12 | 13 | weak var delegate: NoteDelegate? = nil 14 | 15 | lazy var titleField: UITextField = { 16 | let textField = UITextField(frame: .zero) 17 | textField.backgroundColor = .clear 18 | textField.borderStyle = .none 19 | textField.adjustsFontSizeToFitWidth = true 20 | textField.font = UIFont.systemFont(ofSize: 16, weight: .medium) 21 | textField.tintColor = .primaryAction 22 | textField.minimumFontSize = 18 23 | textField.delegate = self 24 | return textField 25 | }() 26 | 27 | lazy var bodyTextView: UITextView = { 28 | let textView = UITextView() 29 | textView.font = UIFont.systemFont(ofSize: 12, weight: .regular) 30 | textView.backgroundColor = .clear 31 | textView.textColor = .lightGray 32 | textView.tintColor = .primaryAction 33 | textView.delegate = self 34 | return textView 35 | }() 36 | 37 | override init(frame: CGRect) { 38 | super.init(frame: frame) 39 | self.backgroundColor = .backgroundCell 40 | setupTitleField() 41 | setupBodyTextView() 42 | } 43 | 44 | required init?(coder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | func setupTitleField() { 49 | self.addSubview(titleField) 50 | titleField.translatesAutoresizingMaskIntoConstraints = false 51 | NSLayoutConstraint.activate([ 52 | titleField.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor), 53 | titleField.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 25), 54 | titleField.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -25), 55 | titleField.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height * 0.05) 56 | ]) 57 | } 58 | 59 | func setupBodyTextView() { 60 | self.addSubview(bodyTextView) 61 | bodyTextView.translatesAutoresizingMaskIntoConstraints = false 62 | NSLayoutConstraint.activate([ 63 | bodyTextView.topAnchor.constraint(equalTo: titleField.bottomAnchor, constant: 5), 64 | bodyTextView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20), 65 | bodyTextView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20), 66 | bodyTextView.bottomAnchor.constraint(equalTo: self.bottomAnchor) 67 | ]) 68 | } 69 | 70 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 71 | super.touchesBegan(touches, with: event) 72 | self.endEditing(true) 73 | } 74 | 75 | } 76 | 77 | 78 | extension NoteView: UITextViewDelegate { 79 | 80 | func textViewDidBeginEditing(_ textView: UITextView) { 81 | textView.textColor = .text 82 | if textView.text == Note.placeholder.body { 83 | textView.text.removeAll() 84 | } 85 | } 86 | 87 | func textViewDidEndEditing(_ textView: UITextView) { 88 | textView.textColor = .lightGray 89 | if textView.text.isEmpty { 90 | textView.text = Note.placeholder.body 91 | } 92 | delegate?.didChange(title: titleField.text!, body: bodyTextView.text!) 93 | } 94 | 95 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 96 | delegate?.didChange(title: titleField.text!, body: bodyTextView.text!+text) 97 | return true 98 | } 99 | 100 | } 101 | 102 | 103 | extension NoteView: UITextFieldDelegate { 104 | 105 | func textFieldDidBeginEditing(_ textField: UITextField) { 106 | if textField.text == Note.placeholder.title { 107 | textField.text!.removeAll() 108 | } 109 | } 110 | 111 | func textFieldDidEndEditing(_ textField: UITextField) { 112 | if textField.text!.isEmpty { 113 | textField.text = Note.placeholder.title 114 | } 115 | delegate?.didChange(title: titleField.text!, body: bodyTextView.text!) 116 | } 117 | 118 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 119 | delegate?.didChange(title: titleField.text!+string, body: bodyTextView.text!) 120 | return true 121 | } 122 | 123 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 124 | self.endEditing(true) 125 | return true 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /PersonalLogs/ViewControllers/ListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListViewController.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ListViewController: UIViewController { 12 | 13 | //MARK: - Model 14 | private var noteRepository: NoteRepository { 15 | NoteRepository() 16 | } 17 | 18 | //MARK: - Views 19 | lazy var listView: ListView = { 20 | let view = ListView() 21 | view.delegate = self 22 | return view 23 | }() 24 | 25 | lazy var newNoteBarButtonItem: UIBarButtonItem = UIBarButtonItem( 26 | image: UIImage(systemName: "plus"), 27 | style: .done, 28 | target: self, 29 | action: #selector(newNote) 30 | ) 31 | 32 | lazy var onboardingBarButtonItem: UIBarButtonItem = UIBarButtonItem( 33 | image: UIImage(systemName: "info"), 34 | style: .plain, 35 | target: self, 36 | action: #selector(showOnboarding) 37 | ) 38 | 39 | //MARK: - Alerts 40 | func showPasswordAlert(okAction: @escaping (String) -> Void) { 41 | let alert = UIAlertController(title: nil, message: "Insert the Secret Password", preferredStyle: .alert) 42 | 43 | alert.addTextField { (textField) in 44 | textField.tintColor = UIColor(named: "primaryActionColor") 45 | textField.textContentType = .password 46 | textField.isSecureTextEntry = true 47 | } 48 | 49 | let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) 50 | alert.addAction(cancelAction) 51 | 52 | let okAction = UIAlertAction(title: "OK", style: .default) { action in 53 | guard let pass = alert.textFields?.first?.text, !pass.isEmpty else { return } 54 | okAction(pass) 55 | } 56 | okAction.setValue(UIColor.primaryAction, forKey: "titleTextColor") 57 | alert.addAction(okAction) 58 | 59 | self.present(alert, animated: true, completion: nil) 60 | } 61 | 62 | func showConfirmationAlert(okAction: @escaping () -> Void) { 63 | let alert = UIAlertController(title: "This operation cannot be undone", message: "Are you sure you want to continue?", preferredStyle: .alert) 64 | 65 | let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) 66 | alert.addAction(cancelAction) 67 | 68 | let okAction = UIAlertAction(title: "OK", style: .default) { action in okAction() } 69 | okAction.setValue(UIColor.primaryAction, forKey: "titleTextColor") 70 | alert.addAction(okAction) 71 | 72 | self.present(alert, animated: true, completion: nil) 73 | } 74 | 75 | func showFadingAlert(message: String) { 76 | let lockedAlert = UIAlertController(title: message, message: nil, preferredStyle: .alert) 77 | self.present(lockedAlert, animated: true, completion: nil) 78 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { 79 | lockedAlert.dismiss(animated: true, completion: nil) 80 | } 81 | } 82 | 83 | //MARK: - ViewController Cycle 84 | override func loadView() { 85 | self.view = listView 86 | } 87 | 88 | override func viewDidLoad() { 89 | super.viewDidLoad() 90 | 91 | // Setting the title that shows on NavigationBar 92 | self.title = "Personal Logs" 93 | 94 | //Adding the newNoteBarButtonItem 95 | navigationItem.rightBarButtonItem = newNoteBarButtonItem 96 | navigationItem.leftBarButtonItem = onboardingBarButtonItem 97 | } 98 | 99 | override func viewWillAppear(_ animated: Bool) { 100 | super.viewWillAppear(animated) 101 | 102 | // Calling the repository to read all notes 103 | listView.notes = noteRepository.readAllItems() 104 | } 105 | 106 | //MARK: - Actions 107 | @objc func showOnboarding() { 108 | navigationController?.present(OnBoardingViewController(), animated: true) 109 | } 110 | 111 | @objc func newNote() { 112 | if let newNote = noteRepository.createNewItem() { 113 | showNoteDetail(id: newNote.id) 114 | } 115 | } 116 | 117 | func showNoteDetail(id: UUID) { 118 | let noteViewController = NoteViewController(noteRepository: noteRepository, id: id) 119 | navigationController?.pushViewController(noteViewController, animated: true) 120 | } 121 | 122 | } 123 | 124 | //MARK: - NoteListDelegate 125 | extension ListViewController: NoteListDelegate { 126 | 127 | func didSelectNote(with id: UUID) { 128 | showNoteDetail(id: id) 129 | } 130 | 131 | func deleteNote(for id: UUID) { 132 | showConfirmationAlert { [weak self] in 133 | self?.noteRepository.delete(id: id) 134 | self?.listView.notes = self?.noteRepository.readAllItems() ?? [] 135 | } 136 | } 137 | 138 | func askForPasswordToLock(for id: UUID) { 139 | showPasswordAlert { [weak self] (password) in 140 | guard let self = self else { return } 141 | 142 | if self.noteRepository.lock(id: id, password: password) { 143 | self.showFadingAlert(message: "Locked 🔒") 144 | } 145 | 146 | self.listView.notes = self.noteRepository.readAllItems() 147 | } 148 | } 149 | 150 | func askForPasswordToUnlock(for id: UUID) { 151 | showPasswordAlert { [weak self] (password) in 152 | guard let self = self else { return } 153 | 154 | if self.noteRepository.unlock(id: id, password: password) { 155 | self.showFadingAlert(message: "Unlocked 🔓") 156 | } else { 157 | self.showFadingAlert(message: "Wrong Password ☠️") 158 | } 159 | 160 | self.listView.notes = self.noteRepository.readAllItems() 161 | } 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /PersonalLogs/View/ListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListView.swift 3 | // PersonalLogs 4 | // 5 | // Created by Gabriela Bezerra on 25/05/20. 6 | // Copyright © 2020 Academy IFCE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ListView: UIView { 12 | 13 | weak var delegate: NoteListDelegate? = nil 14 | 15 | var notes: [Note] = [] { 16 | didSet { 17 | if notes.isEmpty { 18 | setupEmptyState() 19 | } else if tableView.superview == self { 20 | self.tableView.reloadData() 21 | } else { 22 | setupTableView() 23 | } 24 | } 25 | } 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | self.backgroundColor = .background 30 | } 31 | 32 | required init?(coder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | //MARK: - Views 37 | lazy var tableView: UITableView = { 38 | let tableView = UITableView(frame: .zero, style: .insetGrouped) 39 | tableView.backgroundColor = .background 40 | tableView.separatorColor = .text.withAlphaComponent(0.25) 41 | tableView.alwaysBounceVertical = false 42 | tableView.alwaysBounceHorizontal = false 43 | tableView.delegate = self 44 | tableView.dataSource = self 45 | return tableView 46 | }() 47 | 48 | lazy var emptyStateImageView: UIImageView = { 49 | let emptyStateImageView = UIImageView(image: UIImage(named: "emptyState")) 50 | emptyStateImageView.clipsToBounds = false 51 | return emptyStateImageView 52 | }() 53 | 54 | func showDetailOfNote(with id: UUID) { 55 | delegate?.didSelectNote(with: id) 56 | } 57 | 58 | //MARK: Setup Views 59 | func setupTableView() { 60 | emptyStateImageView.removeFromSuperview() 61 | self.addSubview(tableView) 62 | tableView.translatesAutoresizingMaskIntoConstraints = false 63 | NSLayoutConstraint.activate([ 64 | tableView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor), 65 | tableView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 100), 66 | tableView.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor), 67 | tableView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor) 68 | ]) 69 | tableView.reloadData() 70 | } 71 | 72 | func setupEmptyState() { 73 | tableView.removeFromSuperview() 74 | self.addSubview(emptyStateImageView) 75 | emptyStateImageView.translatesAutoresizingMaskIntoConstraints = false 76 | NSLayoutConstraint.activate([ 77 | emptyStateImageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), 78 | emptyStateImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor) 79 | ]) 80 | } 81 | } 82 | 83 | 84 | //MARK: - UITableView Delegate and DataSource 85 | extension ListView: UITableViewDataSource, UITableViewDelegate { 86 | 87 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 88 | notes.count 89 | } 90 | 91 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 92 | 93 | let currentNote = notes[indexPath.row] 94 | 95 | let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) 96 | cell.textLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium) 97 | cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 12, weight: .regular) 98 | cell.detailTextLabel?.numberOfLines = 3 99 | cell.selectionStyle = .none 100 | cell.backgroundColor = .backgroundCell 101 | 102 | cell.textLabel?.text = currentNote.title 103 | cell.detailTextLabel?.text = currentNote.body 104 | 105 | return cell 106 | } 107 | 108 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 109 | let currentNote = notes[indexPath.row] 110 | 111 | let deleteAction = UIContextualAction(style: .destructive, title: "") { 112 | [weak self] (_, _, success) in 113 | self?.delegate?.deleteNote(for: currentNote.id) 114 | success(true) 115 | } 116 | deleteAction.image = UIImage(systemName: "trash.fill") 117 | 118 | return UISwipeActionsConfiguration(actions: [deleteAction]) 119 | } 120 | 121 | func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 122 | let currentNote = notes[indexPath.row] 123 | 124 | let lockAction = UIContextualAction(style: .normal, title: "") { 125 | [weak self] (_, _, success) in 126 | currentNote.locked ? self?.delegate?.askForPasswordToUnlock(for: currentNote.id) : self?.delegate?.askForPasswordToLock(for: currentNote.id) 127 | success(true) 128 | } 129 | 130 | lockAction.backgroundColor = UIColor(named: "primaryActionColor") 131 | lockAction.image = UIImage(systemName: "lock.fill") 132 | 133 | return UISwipeActionsConfiguration(actions: [lockAction]) 134 | } 135 | 136 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 137 | let currentNote = notes[indexPath.row] 138 | 139 | if !currentNote.locked { 140 | showDetailOfNote(with: currentNote.id) 141 | } else { 142 | delegate?.askForPasswordToUnlock(for: currentNote.id) 143 | } 144 | } 145 | 146 | func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 147 | UIView() 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PersonalLogs 2 | 3 | Persistência Básica com Repository em Swift. 4 | 5 | Alguns links para estudo mais prolongado sobre o assunto: 6 | 7 | ### Persistência em geral 8 | 9 | [103 iPS Persistence - DEV](https://dev.to/iphreaks/103-ips-persistence) 10 | 11 | [Apple Getting Started with Persistence](https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/PersistData.html) 12 | 13 | [iOS Data Storage Guidelines - Apple Developer](https://developer.apple.com/icloud/documentation/data-storage/index.html) 14 | 15 | [iOS storage best practices - Vikash Anand - Medium](https://medium.com/@anandin02/ios-storage-best-practices-294fca83ad9) 16 | 17 | 18 | ### App Sandbox 19 | 20 | [App Sandboxing - Apple Developer](https://developer.apple.com/app-sandboxing/) 21 | 22 | [File System Basics](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html) 23 | 24 | [About App Sandbox](https://developer.apple.com/library/archive/documentation/Security/Conceptual/AppSandboxDesignGuide/AboutAppSandbox/AboutAppSandbox.html) 25 | 26 | 27 | ### Archive and Serialization 28 | 29 | [Archives and Serialization | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/archives_and_serialization) 30 | 31 | [Encoding and Decoding Custom Types | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) 32 | 33 | [Codable | Swift by Sundell](https://www.swiftbysundell.com/basics/codable/) 34 | 35 | 36 | ### FileManager 37 | 38 | [FileManager - NSHipster](https://nshipster.com/filemanager/) 39 | 40 | [FileManager - Foundation | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/filemanager) 41 | 42 | [Accessing Files and Directories](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/AccessingFilesandDirectories/AccessingFilesandDirectories.html) 43 | 44 | ### Repository 45 | 46 | [Repository Design Pattern in Swift - Frederik Jacques - Medium](https://medium.com/@frederikjacques/repository-design-pattern-in-swift-952061485aa) 47 | 48 | [Introducing repository pattern with swift. · GitHub](https://gist.github.com/omayib/9f515b6d5a72802bc2e07673788a308d) 49 | 50 | 51 | ### Unit Test on Storage 52 | 53 | [Mocking the File System to Improve Testability (with JustMoc](https://www.telerik.com/blogs/mocking-the-file-system-to-improve-testability-with-justmock) 54 | 55 | [Faster & more robust tests with Xcode 10 | Swift by Sundell](https://www.swiftbysundell.com/wwdc2018/faster-and-more-robust-tests-with-xcode-10/) 56 | 57 | [Mock-free unit tests in Swift | Swift by Sundell](https://www.swiftbysundell.com/articles/mock-free-unit-tests-in-swift/) 58 | 59 | [Mocking for tests in iOS development](https://geek-is-stupid.github.io/2019-04-03-mocking-for-test-in-ios-development/) 60 | 61 | [Avoiding mocking UserDefaults | Swift by Sundell](https://www.swiftbysundell.com/tips/avoiding-mocking-userdefaults/) 62 | 63 | [How to Test Using Fake Data on iOS - Swift2Go - Medium](https://medium.com/swift2go/how-to-test-with-fake-data-on-ios-66dc87bf093e) 64 | 65 | 66 | ### Keychain 67 | 68 | [Keychain Services | Apple Developer Documentation](https://developer.apple.com/documentation/security/keychain_services) 69 | 70 | [Keychains | Apple Developer Documentation](https://developer.apple.com/documentation/security/keychain_services/keychains) 71 | 72 | [Basic iOS Security: Keychain and Hashing | raywenderlich.com](https://www.raywenderlich.com/129-basic-ios-security-keychain-and-hashing) 73 | 74 | [How To Secure iOS User Data: The Keychain and Biometrics – Face ID or Touch ID | raywenderlich.com](https://www.raywenderlich.com/236-how-to-secure-ios-user-data-the-keychain-and-biometrics-face-id-or-touch-id) 75 | 76 | [Keychain Services API Tutorial for Passwords in Swift | raywenderlich.com](https://www.raywenderlich.com/9240-keychain-services-api-tutorial-for-passwords-in-swift) 77 | 78 | [GenericKeychain](https://developer.apple.com/library/archive/samplecode/GenericKeychain/Introduction/Intro.html#//apple_ref/doc/uid/DTS40007797-Intro-DontLinkElementID_2) 79 | 80 | [Storing CryptoKit Keys in the Keychain | Apple Developer Documentation](https://developer.apple.com/documentation/cryptokit/storing_cryptokit_keys_in_the_keychain) 81 | 82 | [Storing Keys in the Keychain | Apple Developer Documentation](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain) 83 | 84 | [Adding a Password to the Keychain | Apple Developer Documentation](https://developer.apple.com/documentation/security/keychain_services/keychain_items/adding_a_password_to_the_keychain) 85 | 86 | [Storing Keys in the Secure Enclave | Apple Developer Documentation](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_secure_enclave) 87 | 88 | [Restricting Keychain Item Accessibility | Apple Developer Documentation](https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility) 89 | 90 | 91 | ### Security / Privacy 92 | 93 | [Security | Apple Developer Documentation](https://developer.apple.com/documentation/security?language=swift) 94 | 95 | [About Software Security](https://developer.apple.com/library/archive/documentation/Security/Conceptual/Security_Overview/Introduction/Introduction.html#//apple_ref/doc/uid/TP30000976) 96 | 97 | [Other Security Resources](https://developer.apple.com/library/archive/documentation/Security/Conceptual/Security_Overview/SeeAlso/SeeAlso.html#//apple_ref/doc/uid/TP30000976-CH7-SW1) 98 | 99 | [App security overview](https://support.apple.com/pt-br/guide/security/sec35dd877d0/1/web/1) 100 | 101 | [Encryption and Data Protection overview - Suporte da Apple](https://support.apple.com/pt-br/guide/security/sece3bee0835/1/web/1) 102 | 103 | [Net Guru](https://www.netguru.com/blog/ios-app-data-security) 104 | 105 | [iOS App Templates](https://www.iosapptemplates.com/blog/ios-development/data-persistence-ios-swift) 106 | 107 | [MOBILE API SECURITY TECHNIQUES PART 1](https://blog.approov.io/mobile-api-security-techniques-part-1) 108 | 109 | [MOBILE API SECURITY TECHNIQUES PART 2](https://blog.approov.io/mobile-api-security-techniques-part-2) 110 | 111 | [MOBILE API SECURITY TECHNIQUES PART 3](https://blog.approov.io/mobile-api-security-techniques-part-3) 112 | 113 | -------------------------------------------------------------------------------- /PersonalLogs/Helpers/Keychain/KeychainPasswordItem.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | A struct for accessing generic password keychain items. 7 | */ 8 | 9 | import Foundation 10 | 11 | struct KeychainPasswordItem { 12 | // MARK: Types 13 | 14 | enum KeychainError: Error { 15 | case noPassword 16 | case unexpectedPasswordData 17 | case unexpectedItemData 18 | case unhandledError(status: OSStatus) 19 | } 20 | 21 | // MARK: Properties 22 | 23 | let service: String 24 | 25 | private(set) var account: String 26 | 27 | let accessGroup: String? 28 | 29 | // MARK: Intialization 30 | 31 | init(service: String, account: String, accessGroup: String? = nil) { 32 | self.service = service 33 | self.account = account 34 | self.accessGroup = accessGroup 35 | } 36 | 37 | // MARK: Keychain access 38 | 39 | func readPassword() throws -> String { 40 | /* 41 | Build a query to find the item that matches the service, account and 42 | access group. 43 | */ 44 | var query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) 45 | query[kSecMatchLimit as String] = kSecMatchLimitOne 46 | query[kSecReturnAttributes as String] = kCFBooleanTrue 47 | query[kSecReturnData as String] = kCFBooleanTrue 48 | 49 | // Try to fetch the existing keychain item that matches the query. 50 | var queryResult: AnyObject? 51 | let status = withUnsafeMutablePointer(to: &queryResult) { 52 | SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) 53 | } 54 | 55 | // Check the return status and throw an error if appropriate. 56 | guard status != errSecItemNotFound else { throw KeychainError.noPassword } 57 | guard status == noErr else { throw KeychainError.unhandledError(status: status) } 58 | 59 | // Parse the password string from the query result. 60 | guard let existingItem = queryResult as? [String : AnyObject], 61 | let passwordData = existingItem[kSecValueData as String] as? Data, 62 | let password = String(data: passwordData, encoding: String.Encoding.utf8) 63 | else { 64 | throw KeychainError.unexpectedPasswordData 65 | } 66 | 67 | return password 68 | } 69 | 70 | func savePassword(_ password: String) throws { 71 | // Encode the password into an Data object. 72 | let encodedPassword = password.data(using: String.Encoding.utf8)! 73 | 74 | do { 75 | // Check for an existing item in the keychain. 76 | try _ = readPassword() 77 | 78 | // Update the existing item with the new password. 79 | var attributesToUpdate = [String : AnyObject]() 80 | attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject? 81 | 82 | let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) 83 | let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) 84 | 85 | // Throw an error if an unexpected status was returned. 86 | guard status == noErr else { throw KeychainError.unhandledError(status: status) } 87 | } 88 | catch KeychainError.noPassword { 89 | /* 90 | No password was found in the keychain. Create a dictionary to save 91 | as a new keychain item. 92 | */ 93 | var newItem = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) 94 | newItem[kSecValueData as String] = encodedPassword as AnyObject? 95 | 96 | // Add a the new item to the keychain. 97 | let status = SecItemAdd(newItem as CFDictionary, nil) 98 | 99 | // Throw an error if an unexpected status was returned. 100 | guard status == noErr else { throw KeychainError.unhandledError(status: status) } 101 | } 102 | } 103 | 104 | mutating func renameAccount(_ newAccountName: String) throws { 105 | // Try to update an existing item with the new account name. 106 | var attributesToUpdate = [String : AnyObject]() 107 | attributesToUpdate[kSecAttrAccount as String] = newAccountName as AnyObject? 108 | 109 | let query = KeychainPasswordItem.keychainQuery(withService: service, account: self.account, accessGroup: accessGroup) 110 | let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) 111 | 112 | // Throw an error if an unexpected status was returned. 113 | guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } 114 | 115 | self.account = newAccountName 116 | } 117 | 118 | func deleteItem() throws { 119 | // Delete the existing item from the keychain. 120 | let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) 121 | let status = SecItemDelete(query as CFDictionary) 122 | 123 | // Throw an error if an unexpected status was returned. 124 | guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } 125 | } 126 | 127 | static func passwordItems(forService service: String, accessGroup: String? = nil) throws -> [KeychainPasswordItem] { 128 | // Build a query for all items that match the service and access group. 129 | var query = KeychainPasswordItem.keychainQuery(withService: service, accessGroup: accessGroup) 130 | query[kSecMatchLimit as String] = kSecMatchLimitAll 131 | query[kSecReturnAttributes as String] = kCFBooleanTrue 132 | query[kSecReturnData as String] = kCFBooleanFalse 133 | 134 | // Fetch matching items from the keychain. 135 | var queryResult: AnyObject? 136 | let status = withUnsafeMutablePointer(to: &queryResult) { 137 | SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) 138 | } 139 | 140 | // If no items were found, return an empty array. 141 | guard status != errSecItemNotFound else { return [] } 142 | 143 | // Throw an error if an unexpected status was returned. 144 | guard status == noErr else { throw KeychainError.unhandledError(status: status) } 145 | 146 | // Cast the query result to an array of dictionaries. 147 | guard let resultData = queryResult as? [[String : AnyObject]] else { throw KeychainError.unexpectedItemData } 148 | 149 | // Create a `KeychainPasswordItem` for each dictionary in the query result. 150 | var passwordItems = [KeychainPasswordItem]() 151 | for result in resultData { 152 | guard let account = result[kSecAttrAccount as String] as? String else { throw KeychainError.unexpectedItemData } 153 | 154 | let passwordItem = KeychainPasswordItem(service: service, account: account, accessGroup: accessGroup) 155 | passwordItems.append(passwordItem) 156 | } 157 | 158 | return passwordItems 159 | } 160 | 161 | // MARK: Convenience 162 | 163 | private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : AnyObject] { 164 | var query = [String : AnyObject]() 165 | query[kSecClass as String] = kSecClassGenericPassword 166 | query[kSecAttrService as String] = service as AnyObject? 167 | 168 | if let account = account { 169 | query[kSecAttrAccount as String] = account as AnyObject? 170 | } 171 | 172 | if let accessGroup = accessGroup { 173 | query[kSecAttrAccessGroup as String] = accessGroup as AnyObject? 174 | } 175 | 176 | return query 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /PersonalLogs.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 064502D6247BD46800AA475E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064502D5247BD46800AA475E /* AppDelegate.swift */; }; 11 | 064502D8247BD46800AA475E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064502D7247BD46800AA475E /* SceneDelegate.swift */; }; 12 | 064502DA247BD46800AA475E /* NoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064502D9247BD46800AA475E /* NoteView.swift */; }; 13 | 064502DC247BD46F00AA475E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 064502DB247BD46F00AA475E /* Assets.xcassets */; }; 14 | 064502DF247BD46F00AA475E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 064502DE247BD46F00AA475E /* Preview Assets.xcassets */; }; 15 | 064502E2247BD46F00AA475E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 064502E0247BD46F00AA475E /* LaunchScreen.storyboard */; }; 16 | 064502ED247BD47000AA475E /* PersonalLogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064502EC247BD47000AA475E /* PersonalLogsTests.swift */; }; 17 | 064502F8247BD4BD00AA475E /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064502F7247BD4BD00AA475E /* ListViewController.swift */; }; 18 | 064502FA247BD4CC00AA475E /* NoteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064502F9247BD4CC00AA475E /* NoteViewController.swift */; }; 19 | 064502FE247BD50000AA475E /* Note.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064502FD247BD50000AA475E /* Note.swift */; }; 20 | 06450302247BD5D200AA475E /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06450301247BD5D200AA475E /* FileHelper.swift */; }; 21 | 06450304247BD5E500AA475E /* NoteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06450303247BD5E500AA475E /* NoteRepository.swift */; }; 22 | 06450308247C2E6C00AA475E /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06450307247C2E6C00AA475E /* ListView.swift */; }; 23 | 0645030A247C657000AA475E /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06450309247C657000AA475E /* ColorExtension.swift */; }; 24 | 0676FB2F247D270C008CB582 /* NoteListDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB2E247D270C008CB582 /* NoteListDelegate.swift */; }; 25 | 0676FB31247D2734008CB582 /* NoteDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB30247D2734008CB582 /* NoteDelegate.swift */; }; 26 | 0676FB33247D9401008CB582 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB32247D9400008CB582 /* Repository.swift */; }; 27 | 0676FB752484B5B9008CB582 /* FileManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB742484B5B9008CB582 /* FileManagerExtension.swift */; }; 28 | 0676FB7A2484B980008CB582 /* OnBoardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB792484B980008CB582 /* OnBoardingViewController.swift */; }; 29 | 0676FB7C2484B994008CB582 /* OnBoardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB7B2484B994008CB582 /* OnBoardingView.swift */; }; 30 | 0676FB7E2484C523008CB582 /* OnBoardingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB7D2484C523008CB582 /* OnBoardingStatus.swift */; }; 31 | 0676FB802484C95B008CB582 /* RepositoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB7F2484C95B008CB582 /* RepositoryItem.swift */; }; 32 | 0676FB822484CD76008CB582 /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB812484CD76008CB582 /* KeychainPasswordItem.swift */; }; 33 | 0676FB842484CE60008CB582 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0676FB832484CE60008CB582 /* KeychainHelper.swift */; }; 34 | /* End PBXBuildFile section */ 35 | 36 | /* Begin PBXContainerItemProxy section */ 37 | 064502E9247BD47000AA475E /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 064502CA247BD46800AA475E /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = 064502D1247BD46800AA475E; 42 | remoteInfo = PersonalLogs; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 064502D2247BD46800AA475E /* Personal Logs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Personal Logs.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 064502D5247BD46800AA475E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49 | 064502D7247BD46800AA475E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 50 | 064502D9247BD46800AA475E /* NoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteView.swift; sourceTree = ""; }; 51 | 064502DB247BD46F00AA475E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52 | 064502DE247BD46F00AA475E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 53 | 064502E1247BD46F00AA475E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 54 | 064502E3247BD46F00AA475E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | 064502E8247BD47000AA475E /* PersonalLogsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PersonalLogsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 064502EC247BD47000AA475E /* PersonalLogsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalLogsTests.swift; sourceTree = ""; }; 57 | 064502EE247BD47000AA475E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | 064502F7247BD4BD00AA475E /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; 59 | 064502F9247BD4CC00AA475E /* NoteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteViewController.swift; sourceTree = ""; }; 60 | 064502FD247BD50000AA475E /* Note.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Note.swift; sourceTree = ""; }; 61 | 06450301247BD5D200AA475E /* FileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = ""; }; 62 | 06450303247BD5E500AA475E /* NoteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteRepository.swift; sourceTree = ""; }; 63 | 06450307247C2E6C00AA475E /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; 64 | 06450309247C657000AA475E /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; 65 | 0676FB2E247D270C008CB582 /* NoteListDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteListDelegate.swift; sourceTree = ""; }; 66 | 0676FB30247D2734008CB582 /* NoteDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteDelegate.swift; sourceTree = ""; }; 67 | 0676FB32247D9400008CB582 /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; 68 | 0676FB742484B5B9008CB582 /* FileManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExtension.swift; sourceTree = ""; }; 69 | 0676FB792484B980008CB582 /* OnBoardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingViewController.swift; sourceTree = ""; }; 70 | 0676FB7B2484B994008CB582 /* OnBoardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingView.swift; sourceTree = ""; }; 71 | 0676FB7D2484C523008CB582 /* OnBoardingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingStatus.swift; sourceTree = ""; }; 72 | 0676FB7F2484C95B008CB582 /* RepositoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryItem.swift; sourceTree = ""; }; 73 | 0676FB812484CD76008CB582 /* KeychainPasswordItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainPasswordItem.swift; sourceTree = ""; }; 74 | 0676FB832484CE60008CB582 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; 75 | /* End PBXFileReference section */ 76 | 77 | /* Begin PBXFrameworksBuildPhase section */ 78 | 064502CF247BD46800AA475E /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | 064502E5247BD47000AA475E /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | /* End PBXFrameworksBuildPhase section */ 93 | 94 | /* Begin PBXGroup section */ 95 | 064502C9247BD46800AA475E = { 96 | isa = PBXGroup; 97 | children = ( 98 | 064502D4247BD46800AA475E /* PersonalLogs */, 99 | 064502EB247BD47000AA475E /* PersonalLogsTests */, 100 | 064502D3247BD46800AA475E /* Products */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 064502D3247BD46800AA475E /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 064502D2247BD46800AA475E /* Personal Logs.app */, 108 | 064502E8247BD47000AA475E /* PersonalLogsTests.xctest */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | 064502D4247BD46800AA475E /* PersonalLogs */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 064502D5247BD46800AA475E /* AppDelegate.swift */, 117 | 064502D7247BD46800AA475E /* SceneDelegate.swift */, 118 | 0676FB2D247D26FB008CB582 /* Protocols */, 119 | 0676FB762484B659008CB582 /* Extensions */, 120 | 06450300247BD5AE00AA475E /* Helpers */, 121 | 064502FF247BD50300AA475E /* Model */, 122 | 064502FB247BD4D100AA475E /* ViewControllers */, 123 | 064502FC247BD4E900AA475E /* View */, 124 | 064502DB247BD46F00AA475E /* Assets.xcassets */, 125 | 064502E0247BD46F00AA475E /* LaunchScreen.storyboard */, 126 | 064502E3247BD46F00AA475E /* Info.plist */, 127 | 064502DD247BD46F00AA475E /* Preview Content */, 128 | ); 129 | path = PersonalLogs; 130 | sourceTree = ""; 131 | }; 132 | 064502DD247BD46F00AA475E /* Preview Content */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 064502DE247BD46F00AA475E /* Preview Assets.xcassets */, 136 | ); 137 | path = "Preview Content"; 138 | sourceTree = ""; 139 | }; 140 | 064502EB247BD47000AA475E /* PersonalLogsTests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 064502EC247BD47000AA475E /* PersonalLogsTests.swift */, 144 | 064502EE247BD47000AA475E /* Info.plist */, 145 | ); 146 | path = PersonalLogsTests; 147 | sourceTree = ""; 148 | }; 149 | 064502FB247BD4D100AA475E /* ViewControllers */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 064502F7247BD4BD00AA475E /* ListViewController.swift */, 153 | 064502F9247BD4CC00AA475E /* NoteViewController.swift */, 154 | 0676FB792484B980008CB582 /* OnBoardingViewController.swift */, 155 | ); 156 | path = ViewControllers; 157 | sourceTree = ""; 158 | }; 159 | 064502FC247BD4E900AA475E /* View */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 064502D9247BD46800AA475E /* NoteView.swift */, 163 | 06450307247C2E6C00AA475E /* ListView.swift */, 164 | 0676FB7B2484B994008CB582 /* OnBoardingView.swift */, 165 | ); 166 | path = View; 167 | sourceTree = ""; 168 | }; 169 | 064502FF247BD50300AA475E /* Model */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 064502FD247BD50000AA475E /* Note.swift */, 173 | 06450303247BD5E500AA475E /* NoteRepository.swift */, 174 | 0676FB7D2484C523008CB582 /* OnBoardingStatus.swift */, 175 | ); 176 | path = Model; 177 | sourceTree = ""; 178 | }; 179 | 06450300247BD5AE00AA475E /* Helpers */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 06450301247BD5D200AA475E /* FileHelper.swift */, 183 | 0676FB852484DFFB008CB582 /* Keychain */, 184 | ); 185 | path = Helpers; 186 | sourceTree = ""; 187 | }; 188 | 0676FB2D247D26FB008CB582 /* Protocols */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | 0676FB7F2484C95B008CB582 /* RepositoryItem.swift */, 192 | 0676FB32247D9400008CB582 /* Repository.swift */, 193 | 0676FB2E247D270C008CB582 /* NoteListDelegate.swift */, 194 | 0676FB30247D2734008CB582 /* NoteDelegate.swift */, 195 | ); 196 | path = Protocols; 197 | sourceTree = ""; 198 | }; 199 | 0676FB762484B659008CB582 /* Extensions */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | 06450309247C657000AA475E /* ColorExtension.swift */, 203 | 0676FB742484B5B9008CB582 /* FileManagerExtension.swift */, 204 | ); 205 | path = Extensions; 206 | sourceTree = ""; 207 | }; 208 | 0676FB852484DFFB008CB582 /* Keychain */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | 0676FB832484CE60008CB582 /* KeychainHelper.swift */, 212 | 0676FB812484CD76008CB582 /* KeychainPasswordItem.swift */, 213 | ); 214 | path = Keychain; 215 | sourceTree = ""; 216 | }; 217 | /* End PBXGroup section */ 218 | 219 | /* Begin PBXNativeTarget section */ 220 | 064502D1247BD46800AA475E /* PersonalLogs */ = { 221 | isa = PBXNativeTarget; 222 | buildConfigurationList = 064502F1247BD47000AA475E /* Build configuration list for PBXNativeTarget "PersonalLogs" */; 223 | buildPhases = ( 224 | 064502CE247BD46800AA475E /* Sources */, 225 | 064502CF247BD46800AA475E /* Frameworks */, 226 | 064502D0247BD46800AA475E /* Resources */, 227 | ); 228 | buildRules = ( 229 | ); 230 | dependencies = ( 231 | ); 232 | name = PersonalLogs; 233 | productName = PersonalLogs; 234 | productReference = 064502D2247BD46800AA475E /* Personal Logs.app */; 235 | productType = "com.apple.product-type.application"; 236 | }; 237 | 064502E7247BD47000AA475E /* PersonalLogsTests */ = { 238 | isa = PBXNativeTarget; 239 | buildConfigurationList = 064502F4247BD47000AA475E /* Build configuration list for PBXNativeTarget "PersonalLogsTests" */; 240 | buildPhases = ( 241 | 064502E4247BD47000AA475E /* Sources */, 242 | 064502E5247BD47000AA475E /* Frameworks */, 243 | 064502E6247BD47000AA475E /* Resources */, 244 | ); 245 | buildRules = ( 246 | ); 247 | dependencies = ( 248 | 064502EA247BD47000AA475E /* PBXTargetDependency */, 249 | ); 250 | name = PersonalLogsTests; 251 | productName = PersonalLogsTests; 252 | productReference = 064502E8247BD47000AA475E /* PersonalLogsTests.xctest */; 253 | productType = "com.apple.product-type.bundle.unit-test"; 254 | }; 255 | /* End PBXNativeTarget section */ 256 | 257 | /* Begin PBXProject section */ 258 | 064502CA247BD46800AA475E /* Project object */ = { 259 | isa = PBXProject; 260 | attributes = { 261 | LastSwiftUpdateCheck = 1150; 262 | LastUpgradeCheck = 1150; 263 | ORGANIZATIONNAME = "Academy IFCE"; 264 | TargetAttributes = { 265 | 064502D1247BD46800AA475E = { 266 | CreatedOnToolsVersion = 11.5; 267 | }; 268 | 064502E7247BD47000AA475E = { 269 | CreatedOnToolsVersion = 11.5; 270 | TestTargetID = 064502D1247BD46800AA475E; 271 | }; 272 | }; 273 | }; 274 | buildConfigurationList = 064502CD247BD46800AA475E /* Build configuration list for PBXProject "PersonalLogs" */; 275 | compatibilityVersion = "Xcode 9.3"; 276 | developmentRegion = en; 277 | hasScannedForEncodings = 0; 278 | knownRegions = ( 279 | en, 280 | Base, 281 | ); 282 | mainGroup = 064502C9247BD46800AA475E; 283 | productRefGroup = 064502D3247BD46800AA475E /* Products */; 284 | projectDirPath = ""; 285 | projectRoot = ""; 286 | targets = ( 287 | 064502D1247BD46800AA475E /* PersonalLogs */, 288 | 064502E7247BD47000AA475E /* PersonalLogsTests */, 289 | ); 290 | }; 291 | /* End PBXProject section */ 292 | 293 | /* Begin PBXResourcesBuildPhase section */ 294 | 064502D0247BD46800AA475E /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | 064502E2247BD46F00AA475E /* LaunchScreen.storyboard in Resources */, 299 | 064502DF247BD46F00AA475E /* Preview Assets.xcassets in Resources */, 300 | 064502DC247BD46F00AA475E /* Assets.xcassets in Resources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | 064502E6247BD47000AA475E /* Resources */ = { 305 | isa = PBXResourcesBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | /* End PBXResourcesBuildPhase section */ 312 | 313 | /* Begin PBXSourcesBuildPhase section */ 314 | 064502CE247BD46800AA475E /* Sources */ = { 315 | isa = PBXSourcesBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | 064502D6247BD46800AA475E /* AppDelegate.swift in Sources */, 319 | 0645030A247C657000AA475E /* ColorExtension.swift in Sources */, 320 | 06450304247BD5E500AA475E /* NoteRepository.swift in Sources */, 321 | 0676FB33247D9401008CB582 /* Repository.swift in Sources */, 322 | 0676FB802484C95B008CB582 /* RepositoryItem.swift in Sources */, 323 | 0676FB7E2484C523008CB582 /* OnBoardingStatus.swift in Sources */, 324 | 064502D8247BD46800AA475E /* SceneDelegate.swift in Sources */, 325 | 0676FB2F247D270C008CB582 /* NoteListDelegate.swift in Sources */, 326 | 064502F8247BD4BD00AA475E /* ListViewController.swift in Sources */, 327 | 0676FB31247D2734008CB582 /* NoteDelegate.swift in Sources */, 328 | 064502FA247BD4CC00AA475E /* NoteViewController.swift in Sources */, 329 | 0676FB7A2484B980008CB582 /* OnBoardingViewController.swift in Sources */, 330 | 0676FB752484B5B9008CB582 /* FileManagerExtension.swift in Sources */, 331 | 064502DA247BD46800AA475E /* NoteView.swift in Sources */, 332 | 06450302247BD5D200AA475E /* FileHelper.swift in Sources */, 333 | 0676FB822484CD76008CB582 /* KeychainPasswordItem.swift in Sources */, 334 | 0676FB7C2484B994008CB582 /* OnBoardingView.swift in Sources */, 335 | 064502FE247BD50000AA475E /* Note.swift in Sources */, 336 | 06450308247C2E6C00AA475E /* ListView.swift in Sources */, 337 | 0676FB842484CE60008CB582 /* KeychainHelper.swift in Sources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | 064502E4247BD47000AA475E /* Sources */ = { 342 | isa = PBXSourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | 064502ED247BD47000AA475E /* PersonalLogsTests.swift in Sources */, 346 | ); 347 | runOnlyForDeploymentPostprocessing = 0; 348 | }; 349 | /* End PBXSourcesBuildPhase section */ 350 | 351 | /* Begin PBXTargetDependency section */ 352 | 064502EA247BD47000AA475E /* PBXTargetDependency */ = { 353 | isa = PBXTargetDependency; 354 | target = 064502D1247BD46800AA475E /* PersonalLogs */; 355 | targetProxy = 064502E9247BD47000AA475E /* PBXContainerItemProxy */; 356 | }; 357 | /* End PBXTargetDependency section */ 358 | 359 | /* Begin PBXVariantGroup section */ 360 | 064502E0247BD46F00AA475E /* LaunchScreen.storyboard */ = { 361 | isa = PBXVariantGroup; 362 | children = ( 363 | 064502E1247BD46F00AA475E /* Base */, 364 | ); 365 | name = LaunchScreen.storyboard; 366 | sourceTree = ""; 367 | }; 368 | /* End PBXVariantGroup section */ 369 | 370 | /* Begin XCBuildConfiguration section */ 371 | 064502EF247BD47000AA475E /* Debug */ = { 372 | isa = XCBuildConfiguration; 373 | buildSettings = { 374 | ALWAYS_SEARCH_USER_PATHS = NO; 375 | CLANG_ANALYZER_NONNULL = YES; 376 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 377 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 378 | CLANG_CXX_LIBRARY = "libc++"; 379 | CLANG_ENABLE_MODULES = YES; 380 | CLANG_ENABLE_OBJC_ARC = YES; 381 | CLANG_ENABLE_OBJC_WEAK = YES; 382 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 383 | CLANG_WARN_BOOL_CONVERSION = YES; 384 | CLANG_WARN_COMMA = YES; 385 | CLANG_WARN_CONSTANT_CONVERSION = YES; 386 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 387 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 388 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 389 | CLANG_WARN_EMPTY_BODY = YES; 390 | CLANG_WARN_ENUM_CONVERSION = YES; 391 | CLANG_WARN_INFINITE_RECURSION = YES; 392 | CLANG_WARN_INT_CONVERSION = YES; 393 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 394 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 395 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 397 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 398 | CLANG_WARN_STRICT_PROTOTYPES = YES; 399 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 400 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 401 | CLANG_WARN_UNREACHABLE_CODE = YES; 402 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 403 | COPY_PHASE_STRIP = NO; 404 | DEBUG_INFORMATION_FORMAT = dwarf; 405 | ENABLE_STRICT_OBJC_MSGSEND = YES; 406 | ENABLE_TESTABILITY = YES; 407 | GCC_C_LANGUAGE_STANDARD = gnu11; 408 | GCC_DYNAMIC_NO_PIC = NO; 409 | GCC_NO_COMMON_BLOCKS = YES; 410 | GCC_OPTIMIZATION_LEVEL = 0; 411 | GCC_PREPROCESSOR_DEFINITIONS = ( 412 | "DEBUG=1", 413 | "$(inherited)", 414 | ); 415 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 416 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 417 | GCC_WARN_UNDECLARED_SELECTOR = YES; 418 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 419 | GCC_WARN_UNUSED_FUNCTION = YES; 420 | GCC_WARN_UNUSED_VARIABLE = YES; 421 | IPHONEOS_DEPLOYMENT_TARGET = 13; 422 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 423 | MTL_FAST_MATH = YES; 424 | ONLY_ACTIVE_ARCH = YES; 425 | SDKROOT = iphoneos; 426 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 427 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 428 | }; 429 | name = Debug; 430 | }; 431 | 064502F0247BD47000AA475E /* Release */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ALWAYS_SEARCH_USER_PATHS = NO; 435 | CLANG_ANALYZER_NONNULL = YES; 436 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 437 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 438 | CLANG_CXX_LIBRARY = "libc++"; 439 | CLANG_ENABLE_MODULES = YES; 440 | CLANG_ENABLE_OBJC_ARC = YES; 441 | CLANG_ENABLE_OBJC_WEAK = YES; 442 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 443 | CLANG_WARN_BOOL_CONVERSION = YES; 444 | CLANG_WARN_COMMA = YES; 445 | CLANG_WARN_CONSTANT_CONVERSION = YES; 446 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 447 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 448 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 449 | CLANG_WARN_EMPTY_BODY = YES; 450 | CLANG_WARN_ENUM_CONVERSION = YES; 451 | CLANG_WARN_INFINITE_RECURSION = YES; 452 | CLANG_WARN_INT_CONVERSION = YES; 453 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 454 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 455 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 456 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 457 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 458 | CLANG_WARN_STRICT_PROTOTYPES = YES; 459 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 460 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 461 | CLANG_WARN_UNREACHABLE_CODE = YES; 462 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 463 | COPY_PHASE_STRIP = NO; 464 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 465 | ENABLE_NS_ASSERTIONS = NO; 466 | ENABLE_STRICT_OBJC_MSGSEND = YES; 467 | GCC_C_LANGUAGE_STANDARD = gnu11; 468 | GCC_NO_COMMON_BLOCKS = YES; 469 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 470 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 471 | GCC_WARN_UNDECLARED_SELECTOR = YES; 472 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 473 | GCC_WARN_UNUSED_FUNCTION = YES; 474 | GCC_WARN_UNUSED_VARIABLE = YES; 475 | IPHONEOS_DEPLOYMENT_TARGET = 13; 476 | MTL_ENABLE_DEBUG_INFO = NO; 477 | MTL_FAST_MATH = YES; 478 | SDKROOT = iphoneos; 479 | SWIFT_COMPILATION_MODE = wholemodule; 480 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 481 | VALIDATE_PRODUCT = YES; 482 | }; 483 | name = Release; 484 | }; 485 | 064502F2247BD47000AA475E /* Debug */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 489 | CODE_SIGN_STYLE = Automatic; 490 | DEVELOPMENT_ASSET_PATHS = "\"PersonalLogs/Preview Content\""; 491 | DEVELOPMENT_TEAM = 526YR6WHWC; 492 | ENABLE_PREVIEWS = YES; 493 | INFOPLIST_FILE = PersonalLogs/Info.plist; 494 | LD_RUNPATH_SEARCH_PATHS = ( 495 | "$(inherited)", 496 | "@executable_path/Frameworks", 497 | ); 498 | PRODUCT_BUNDLE_IDENTIFIER = academy.IFCE.PersonalLogs; 499 | PRODUCT_NAME = "Personal Logs"; 500 | SWIFT_VERSION = 5.0; 501 | TARGETED_DEVICE_FAMILY = "1,2"; 502 | }; 503 | name = Debug; 504 | }; 505 | 064502F3247BD47000AA475E /* Release */ = { 506 | isa = XCBuildConfiguration; 507 | buildSettings = { 508 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 509 | CODE_SIGN_STYLE = Automatic; 510 | DEVELOPMENT_ASSET_PATHS = "\"PersonalLogs/Preview Content\""; 511 | DEVELOPMENT_TEAM = 526YR6WHWC; 512 | ENABLE_PREVIEWS = YES; 513 | INFOPLIST_FILE = PersonalLogs/Info.plist; 514 | LD_RUNPATH_SEARCH_PATHS = ( 515 | "$(inherited)", 516 | "@executable_path/Frameworks", 517 | ); 518 | PRODUCT_BUNDLE_IDENTIFIER = academy.IFCE.PersonalLogs; 519 | PRODUCT_NAME = "Personal Logs"; 520 | SWIFT_VERSION = 5.0; 521 | TARGETED_DEVICE_FAMILY = "1,2"; 522 | }; 523 | name = Release; 524 | }; 525 | 064502F5247BD47000AA475E /* Debug */ = { 526 | isa = XCBuildConfiguration; 527 | buildSettings = { 528 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 529 | BUNDLE_LOADER = "$(TEST_HOST)"; 530 | CODE_SIGN_STYLE = Automatic; 531 | DEVELOPMENT_TEAM = 526YR6WHWC; 532 | INFOPLIST_FILE = PersonalLogsTests/Info.plist; 533 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 534 | LD_RUNPATH_SEARCH_PATHS = ( 535 | "$(inherited)", 536 | "@executable_path/Frameworks", 537 | "@loader_path/Frameworks", 538 | ); 539 | PRODUCT_BUNDLE_IDENTIFIER = academy.IFCE.PersonalLogsTests; 540 | PRODUCT_NAME = "$(TARGET_NAME)"; 541 | SWIFT_VERSION = 5.0; 542 | TARGETED_DEVICE_FAMILY = "1,2"; 543 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PersonalLogs.app/PersonalLogs"; 544 | }; 545 | name = Debug; 546 | }; 547 | 064502F6247BD47000AA475E /* Release */ = { 548 | isa = XCBuildConfiguration; 549 | buildSettings = { 550 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 551 | BUNDLE_LOADER = "$(TEST_HOST)"; 552 | CODE_SIGN_STYLE = Automatic; 553 | DEVELOPMENT_TEAM = 526YR6WHWC; 554 | INFOPLIST_FILE = PersonalLogsTests/Info.plist; 555 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 556 | LD_RUNPATH_SEARCH_PATHS = ( 557 | "$(inherited)", 558 | "@executable_path/Frameworks", 559 | "@loader_path/Frameworks", 560 | ); 561 | PRODUCT_BUNDLE_IDENTIFIER = academy.IFCE.PersonalLogsTests; 562 | PRODUCT_NAME = "$(TARGET_NAME)"; 563 | SWIFT_VERSION = 5.0; 564 | TARGETED_DEVICE_FAMILY = "1,2"; 565 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PersonalLogs.app/PersonalLogs"; 566 | }; 567 | name = Release; 568 | }; 569 | /* End XCBuildConfiguration section */ 570 | 571 | /* Begin XCConfigurationList section */ 572 | 064502CD247BD46800AA475E /* Build configuration list for PBXProject "PersonalLogs" */ = { 573 | isa = XCConfigurationList; 574 | buildConfigurations = ( 575 | 064502EF247BD47000AA475E /* Debug */, 576 | 064502F0247BD47000AA475E /* Release */, 577 | ); 578 | defaultConfigurationIsVisible = 0; 579 | defaultConfigurationName = Release; 580 | }; 581 | 064502F1247BD47000AA475E /* Build configuration list for PBXNativeTarget "PersonalLogs" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | 064502F2247BD47000AA475E /* Debug */, 585 | 064502F3247BD47000AA475E /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | 064502F4247BD47000AA475E /* Build configuration list for PBXNativeTarget "PersonalLogsTests" */ = { 591 | isa = XCConfigurationList; 592 | buildConfigurations = ( 593 | 064502F5247BD47000AA475E /* Debug */, 594 | 064502F6247BD47000AA475E /* Release */, 595 | ); 596 | defaultConfigurationIsVisible = 0; 597 | defaultConfigurationName = Release; 598 | }; 599 | /* End XCConfigurationList section */ 600 | }; 601 | rootObject = 064502CA247BD46800AA475E /* Project object */; 602 | } 603 | --------------------------------------------------------------------------------