├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── LICENSE ├── MobileNoteTaker ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── MobileNoteTaker.entitlements └── NotesTableViewController.swift ├── NoteTaker.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── NoteTakerCore.xcscheme ├── NoteTaker ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── compose.imageset │ │ ├── Contents.json │ │ └── compose@2x.png │ └── share.imageset │ │ ├── Contents.json │ │ └── share@2x.png ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── NSTableView+Diff.h ├── NSTableView+Diff.m ├── NSTableView+Rx.swift ├── NSToolbarFlexibleSpaceItem.h ├── NeedsStorage.swift ├── NoteEditorViewController.swift ├── NoteTaker-Bridging-Header.h ├── NoteTaker.entitlements ├── NotesSplitViewController.swift └── NotesTableViewController.swift ├── NoteTakerCore ├── Info.plist ├── NoteTakerCore.h ├── Resources │ └── Editor.html └── Source │ ├── Note+RealmNote.swift │ ├── Note.swift │ ├── NoteEditorView.swift │ ├── NoteViewModel.swift │ ├── NotesStorage.swift │ ├── NotesSyncEngine.swift │ ├── RealmNote+CKRecord.swift │ ├── RealmNote.swift │ └── String+HTML.swift ├── README.md └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __MACOSX 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | DerivedData 17 | .idea/ 18 | Crashlytics.sh 19 | generatechangelog.sh 20 | Pods/ 21 | Carthage -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveX/RxSwift" ~> 3.0 2 | github "realm/realm-cocoa" ~> 2.4 3 | github "RxSwiftCommunity/RxRealm" ~> 0.5 4 | github "RxSwiftCommunity/RxOptional" ~> 3.1.3 5 | github "insidegui/IGListKit" "incrementalmove" -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "insidegui/IGListKit" "4afbc24fc03ee57f7b4879960307a8ae22f3cb14" 2 | github "ReactiveX/RxSwift" "3.2.0" 3 | github "realm/realm-cocoa" "v2.4.3" 4 | github "RxSwiftCommunity/RxOptional" "3.1.3" 5 | github "RxSwiftCommunity/RxRealm" "0.5.1" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Peixe Urbano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MobileNoteTaker/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MobileNoteTaker 4 | // 5 | // Created by Guilherme Rambo on 27/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NoteTakerCore 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | private var syncEngine: NotesSyncEngine! 16 | private let storage = NotesStorage() 17 | 18 | var window: UIWindow? 19 | private var navigationController: UINavigationController! 20 | 21 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 22 | UIApplication.shared.registerForRemoteNotifications() 23 | 24 | setup() 25 | 26 | syncEngine = NotesSyncEngine(storage: storage) 27 | 28 | return true 29 | } 30 | 31 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 32 | NotificationCenter.default.post(name: .notesDidChangeRemotely, object: nil, userInfo: userInfo) 33 | } 34 | 35 | func setup() { 36 | window = UIWindow(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) 37 | window!.backgroundColor = .white 38 | 39 | let notesController = NotesTableViewController(storage: storage) 40 | 41 | navigationController = UINavigationController(rootViewController: notesController) 42 | 43 | window!.rootViewController = navigationController 44 | 45 | window!.makeKeyAndVisible() 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /MobileNoteTaker/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /MobileNoteTaker/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 | -------------------------------------------------------------------------------- /MobileNoteTaker/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIBackgroundModes 24 | 25 | fetch 26 | remote-notification 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /MobileNoteTaker/MobileNoteTaker.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | iCloud.br.com.guilhermerambo.NoteTakerShared 10 | 11 | com.apple.developer.icloud-services 12 | 13 | CloudKit 14 | 15 | com.apple.developer.ubiquity-kvstore-identifier 16 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 17 | 18 | 19 | -------------------------------------------------------------------------------- /MobileNoteTaker/NotesTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesTableViewController.swift 3 | // MobileNoteTaker 4 | // 5 | // Created by Guilherme Rambo on 27/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import NoteTakerCore 13 | 14 | class NotesTableViewController: UITableViewController { 15 | 16 | private let storage: NotesStorage 17 | 18 | init(storage: NotesStorage) { 19 | self.storage = storage 20 | 21 | super.init(style: .plain) 22 | 23 | self.title = "NoteTaker" 24 | } 25 | 26 | required init?(coder aDecoder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | private let disposeBag = DisposeBag() 31 | 32 | var notes = Variable<[Note]>([]) 33 | 34 | var viewModels = [NoteViewModel]() { 35 | didSet { 36 | tableView.reloadData() 37 | // tableView.reloadData(withOldValue: oldValue, newValue: viewModels) 38 | } 39 | } 40 | 41 | private struct Constants { 42 | static let cellIdentifier = "note" 43 | } 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | 48 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.cellIdentifier) 49 | 50 | storage.allNotes.asObservable() 51 | .observeOn(MainScheduler.instance) 52 | .bindTo(notes) 53 | .addDisposableTo(disposeBag) 54 | 55 | notes.asObservable() 56 | .map({ $0.map(NoteViewModel.init) }) 57 | .subscribe { [weak self] event in 58 | switch event { 59 | case .next(let noteViewModels): 60 | self?.viewModels = noteViewModels 61 | break 62 | default: break 63 | } 64 | }.addDisposableTo(disposeBag) 65 | } 66 | 67 | override func numberOfSections(in tableView: UITableView) -> Int { 68 | return 1 69 | } 70 | 71 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 72 | return viewModels.count 73 | } 74 | 75 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 76 | let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellIdentifier) 77 | 78 | cell?.accessoryType = .disclosureIndicator 79 | cell?.textLabel?.text = viewModels[indexPath.row].title 80 | 81 | return cell ?? UITableViewCell() 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /NoteTaker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DD02010C1E4E583500C4D40D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02010B1E4E583500C4D40D /* AppDelegate.swift */; }; 11 | DD0201101E4E583500C4D40D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD02010F1E4E583500C4D40D /* Assets.xcassets */; }; 12 | DD0201131E4E583500C4D40D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD0201111E4E583500C4D40D /* Main.storyboard */; }; 13 | DD0201221E4E593A00C4D40D /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD02011D1E4E593A00C4D40D /* RxCocoa.framework */; }; 14 | DD0201241E4E593A00C4D40D /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD02011F1E4E593A00C4D40D /* RxSwift.framework */; }; 15 | DD02013D1E4E8D2F00C4D40D /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD02013C1E4E8D2F00C4D40D /* CloudKit.framework */; }; 16 | DD02013F1E4E8EBC00C4D40D /* NotesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02013E1E4E8EBC00C4D40D /* NotesTableViewController.swift */; }; 17 | DD0201421E4E8EC500C4D40D /* NoteEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0201401E4E8EC500C4D40D /* NoteEditorViewController.swift */; }; 18 | DD1A3D081E4E92910068347D /* NotesSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A3D071E4E92910068347D /* NotesSplitViewController.swift */; }; 19 | DD1A3D0A1E4E99020068347D /* IGListKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1A3D091E4E99020068347D /* IGListKit.framework */; }; 20 | DD1A3D141E4EA1110068347D /* NSTableView+Diff.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1A3D131E4EA1110068347D /* NSTableView+Diff.m */; }; 21 | DD1A3D191E4EA4520068347D /* NeedsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A3D181E4EA4520068347D /* NeedsStorage.swift */; }; 22 | DD6980471E4F4C9700A22EDC /* NSTableView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6980461E4F4C9700A22EDC /* NSTableView+Rx.swift */; }; 23 | DD6980491E4FC5B200A22EDC /* RxOptional.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD6980481E4FC5B200A22EDC /* RxOptional.framework */; }; 24 | DDD631951E64974E00937A6A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD631941E64974E00937A6A /* AppDelegate.swift */; }; 25 | DDD631971E64974E00937A6A /* NotesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD631961E64974E00937A6A /* NotesTableViewController.swift */; }; 26 | DDD6319C1E64974E00937A6A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDD6319B1E64974E00937A6A /* Assets.xcassets */; }; 27 | DDD6319F1E64974E00937A6A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDD6319D1E64974E00937A6A /* LaunchScreen.storyboard */; }; 28 | DDD631A61E64976D00937A6A /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDD631A51E64976D00937A6A /* CloudKit.framework */; }; 29 | DDD631AF1E64987D00937A6A /* IGListKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDD631A81E64984900937A6A /* IGListKit.framework */; }; 30 | DDD631B01E64988300937A6A /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDD631AB1E64984900937A6A /* RxCocoa.framework */; }; 31 | DDD631B11E64988600937A6A /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDD631AE1E64984900937A6A /* RxSwift.framework */; }; 32 | DDD631B21E64988D00937A6A /* RxOptional.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDD631AC1E64984900937A6A /* RxOptional.framework */; }; 33 | DDD631B51E6498F700937A6A /* NotesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E951E647EFD00B30065 /* NotesStorage.swift */; }; 34 | DDD631B61E6498F700937A6A /* NoteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E911E647EEE00B30065 /* NoteViewModel.swift */; }; 35 | DDD631B71E6498F700937A6A /* Note.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E891E647EDE00B30065 /* Note.swift */; }; 36 | DDD631B81E6498F700937A6A /* Note+RealmNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E8A1E647EDE00B30065 /* Note+RealmNote.swift */; }; 37 | DDD631B91E6498F700937A6A /* NotesSyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E961E647EFD00B30065 /* NotesSyncEngine.swift */; }; 38 | DDD631BA1E6498F700937A6A /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E931E647EF300B30065 /* String+HTML.swift */; }; 39 | DDD631BB1E6498F700937A6A /* RealmNote+CKRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E8C1E647EDE00B30065 /* RealmNote+CKRecord.swift */; }; 40 | DDD631BC1E6498F700937A6A /* RealmNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E8B1E647EDE00B30065 /* RealmNote.swift */; }; 41 | DDD631C31E6498F700937A6A /* NoteTakerCore.h in Headers */ = {isa = PBXBuildFile; fileRef = DDE82E7A1E647E7800B30065 /* NoteTakerCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 42 | DDD631CA1E64995500937A6A /* NoteTakerCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDD631C81E6498F700937A6A /* NoteTakerCore.framework */; }; 43 | DDD631CB1E64995500937A6A /* NoteTakerCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DDD631C81E6498F700937A6A /* NoteTakerCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 44 | DDD631D11E649B4100937A6A /* NoteEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD631D01E649B4100937A6A /* NoteEditorView.swift */; }; 45 | DDD631D41E649D7F00937A6A /* Editor.html in Resources */ = {isa = PBXBuildFile; fileRef = DDD631D31E649D7F00937A6A /* Editor.html */; }; 46 | DDD631D51E649D7F00937A6A /* Editor.html in Resources */ = {isa = PBXBuildFile; fileRef = DDD631D31E649D7F00937A6A /* Editor.html */; }; 47 | DDD631D61E64A38B00937A6A /* NoteEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD631D01E649B4100937A6A /* NoteEditorView.swift */; }; 48 | DDE82E7C1E647E7800B30065 /* NoteTakerCore.h in Headers */ = {isa = PBXBuildFile; fileRef = DDE82E7A1E647E7800B30065 /* NoteTakerCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 49 | DDE82E7F1E647E7800B30065 /* NoteTakerCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDE82E781E647E7800B30065 /* NoteTakerCore.framework */; }; 50 | DDE82E811E647E7800B30065 /* NoteTakerCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DDE82E781E647E7800B30065 /* NoteTakerCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 51 | DDE82E8D1E647EDE00B30065 /* Note.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E891E647EDE00B30065 /* Note.swift */; }; 52 | DDE82E8E1E647EDE00B30065 /* Note+RealmNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E8A1E647EDE00B30065 /* Note+RealmNote.swift */; }; 53 | DDE82E8F1E647EDE00B30065 /* RealmNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E8B1E647EDE00B30065 /* RealmNote.swift */; }; 54 | DDE82E901E647EDE00B30065 /* RealmNote+CKRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E8C1E647EDE00B30065 /* RealmNote+CKRecord.swift */; }; 55 | DDE82E921E647EEE00B30065 /* NoteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E911E647EEE00B30065 /* NoteViewModel.swift */; }; 56 | DDE82E941E647EF300B30065 /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E931E647EF300B30065 /* String+HTML.swift */; }; 57 | DDE82E971E647EFD00B30065 /* NotesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E951E647EFD00B30065 /* NotesStorage.swift */; }; 58 | DDE82E981E647EFD00B30065 /* NotesSyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE82E961E647EFD00B30065 /* NotesSyncEngine.swift */; }; 59 | DDE82E9A1E647F1900B30065 /* Realm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD02011B1E4E593A00C4D40D /* Realm.framework */; }; 60 | DDE82E9B1E647F1B00B30065 /* RealmSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD02011C1E4E593A00C4D40D /* RealmSwift.framework */; }; 61 | DDE82E9C1E647F1F00B30065 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD02011F1E4E593A00C4D40D /* RxSwift.framework */; }; 62 | DDE82E9D1E647F2200B30065 /* IGListKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1A3D091E4E99020068347D /* IGListKit.framework */; }; 63 | /* End PBXBuildFile section */ 64 | 65 | /* Begin PBXContainerItemProxy section */ 66 | DDD631CC1E64995500937A6A /* PBXContainerItemProxy */ = { 67 | isa = PBXContainerItemProxy; 68 | containerPortal = DD0201001E4E583500C4D40D /* Project object */; 69 | proxyType = 1; 70 | remoteGlobalIDString = DDD631B31E6498F700937A6A; 71 | remoteInfo = "NoteTakerCore-iOS"; 72 | }; 73 | DDE82E7D1E647E7800B30065 /* PBXContainerItemProxy */ = { 74 | isa = PBXContainerItemProxy; 75 | containerPortal = DD0201001E4E583500C4D40D /* Project object */; 76 | proxyType = 1; 77 | remoteGlobalIDString = DDE82E771E647E7800B30065; 78 | remoteInfo = NoteTakerCore; 79 | }; 80 | /* End PBXContainerItemProxy section */ 81 | 82 | /* Begin PBXCopyFilesBuildPhase section */ 83 | DD0201261E4E59B900C4D40D /* Carthage: Copy DSYMs */ = { 84 | isa = PBXCopyFilesBuildPhase; 85 | buildActionMask = 2147483647; 86 | dstPath = ""; 87 | dstSubfolderSpec = 16; 88 | files = ( 89 | ); 90 | name = "Carthage: Copy DSYMs"; 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | DDD631CE1E64995500937A6A /* Embed Frameworks */ = { 94 | isa = PBXCopyFilesBuildPhase; 95 | buildActionMask = 2147483647; 96 | dstPath = ""; 97 | dstSubfolderSpec = 10; 98 | files = ( 99 | DDD631CB1E64995500937A6A /* NoteTakerCore.framework in Embed Frameworks */, 100 | ); 101 | name = "Embed Frameworks"; 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | DDE82E801E647E7800B30065 /* Embed Frameworks */ = { 105 | isa = PBXCopyFilesBuildPhase; 106 | buildActionMask = 2147483647; 107 | dstPath = ""; 108 | dstSubfolderSpec = 10; 109 | files = ( 110 | DDE82E811E647E7800B30065 /* NoteTakerCore.framework in Embed Frameworks */, 111 | ); 112 | name = "Embed Frameworks"; 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXCopyFilesBuildPhase section */ 116 | 117 | /* Begin PBXFileReference section */ 118 | DD0201081E4E583500C4D40D /* NoteTaker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NoteTaker.app; sourceTree = BUILT_PRODUCTS_DIR; }; 119 | DD02010B1E4E583500C4D40D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 120 | DD02010F1E4E583500C4D40D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 121 | DD0201121E4E583500C4D40D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 122 | DD0201141E4E583500C4D40D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 123 | DD02011B1E4E593A00C4D40D /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = Carthage/Build/Mac/Realm.framework; sourceTree = ""; }; 124 | DD02011C1E4E593A00C4D40D /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = Carthage/Build/Mac/RealmSwift.framework; sourceTree = ""; }; 125 | DD02011D1E4E593A00C4D40D /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/Mac/RxCocoa.framework; sourceTree = ""; }; 126 | DD02011E1E4E593A00C4D40D /* RxRealm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxRealm.framework; path = Carthage/Build/Mac/RxRealm.framework; sourceTree = ""; }; 127 | DD02011F1E4E593A00C4D40D /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = Carthage/Build/Mac/RxSwift.framework; sourceTree = ""; }; 128 | DD02013B1E4E8D2700C4D40D /* NoteTaker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NoteTaker.entitlements; sourceTree = ""; }; 129 | DD02013C1E4E8D2F00C4D40D /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 130 | DD02013E1E4E8EBC00C4D40D /* NotesTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotesTableViewController.swift; sourceTree = ""; }; 131 | DD0201401E4E8EC500C4D40D /* NoteEditorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteEditorViewController.swift; sourceTree = ""; }; 132 | DD1A3D071E4E92910068347D /* NotesSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotesSplitViewController.swift; sourceTree = ""; }; 133 | DD1A3D091E4E99020068347D /* IGListKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IGListKit.framework; path = Carthage/Build/Mac/IGListKit.framework; sourceTree = ""; }; 134 | DD1A3D111E4EA1110068347D /* NoteTaker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NoteTaker-Bridging-Header.h"; sourceTree = ""; }; 135 | DD1A3D121E4EA1110068347D /* NSTableView+Diff.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSTableView+Diff.h"; sourceTree = ""; }; 136 | DD1A3D131E4EA1110068347D /* NSTableView+Diff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSTableView+Diff.m"; sourceTree = ""; }; 137 | DD1A3D181E4EA4520068347D /* NeedsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeedsStorage.swift; sourceTree = ""; }; 138 | DD6980461E4F4C9700A22EDC /* NSTableView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTableView+Rx.swift"; sourceTree = ""; }; 139 | DD6980481E4FC5B200A22EDC /* RxOptional.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxOptional.framework; path = Carthage/Build/Mac/RxOptional.framework; sourceTree = ""; }; 140 | DDCDA0AD1E58AE44002B63CB /* NSToolbarFlexibleSpaceItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSToolbarFlexibleSpaceItem.h; sourceTree = ""; }; 141 | DDD631921E64974E00937A6A /* MobileNoteTaker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MobileNoteTaker.app; sourceTree = BUILT_PRODUCTS_DIR; }; 142 | DDD631941E64974E00937A6A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 143 | DDD631961E64974E00937A6A /* NotesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesTableViewController.swift; sourceTree = ""; }; 144 | DDD6319B1E64974E00937A6A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 145 | DDD6319E1E64974E00937A6A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 146 | DDD631A01E64974E00937A6A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 147 | DDD631A41E64976400937A6A /* MobileNoteTaker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MobileNoteTaker.entitlements; sourceTree = ""; }; 148 | DDD631A51E64976D00937A6A /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; }; 149 | DDD631A81E64984900937A6A /* IGListKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IGListKit.framework; path = Carthage/Build/iOS/IGListKit.framework; sourceTree = ""; }; 150 | DDD631A91E64984900937A6A /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = Carthage/Build/iOS/Realm.framework; sourceTree = ""; }; 151 | DDD631AA1E64984900937A6A /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = Carthage/Build/iOS/RealmSwift.framework; sourceTree = ""; }; 152 | DDD631AB1E64984900937A6A /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/iOS/RxCocoa.framework; sourceTree = ""; }; 153 | DDD631AC1E64984900937A6A /* RxOptional.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxOptional.framework; path = Carthage/Build/iOS/RxOptional.framework; sourceTree = ""; }; 154 | DDD631AD1E64984900937A6A /* RxRealm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxRealm.framework; path = Carthage/Build/iOS/RxRealm.framework; sourceTree = ""; }; 155 | DDD631AE1E64984900937A6A /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = Carthage/Build/iOS/RxSwift.framework; sourceTree = ""; }; 156 | DDD631C81E6498F700937A6A /* NoteTakerCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NoteTakerCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 157 | DDD631D01E649B4100937A6A /* NoteEditorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NoteEditorView.swift; path = Source/NoteEditorView.swift; sourceTree = ""; }; 158 | DDD631D31E649D7F00937A6A /* Editor.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = Editor.html; path = Resources/Editor.html; sourceTree = ""; }; 159 | DDE82E781E647E7800B30065 /* NoteTakerCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NoteTakerCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 160 | DDE82E7A1E647E7800B30065 /* NoteTakerCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NoteTakerCore.h; sourceTree = ""; }; 161 | DDE82E7B1E647E7800B30065 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 162 | DDE82E891E647EDE00B30065 /* Note.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Note.swift; path = Source/Note.swift; sourceTree = ""; }; 163 | DDE82E8A1E647EDE00B30065 /* Note+RealmNote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Note+RealmNote.swift"; path = "Source/Note+RealmNote.swift"; sourceTree = ""; }; 164 | DDE82E8B1E647EDE00B30065 /* RealmNote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RealmNote.swift; path = Source/RealmNote.swift; sourceTree = ""; }; 165 | DDE82E8C1E647EDE00B30065 /* RealmNote+CKRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "RealmNote+CKRecord.swift"; path = "Source/RealmNote+CKRecord.swift"; sourceTree = ""; }; 166 | DDE82E911E647EEE00B30065 /* NoteViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NoteViewModel.swift; path = Source/NoteViewModel.swift; sourceTree = ""; }; 167 | DDE82E931E647EF300B30065 /* String+HTML.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "String+HTML.swift"; path = "Source/String+HTML.swift"; sourceTree = ""; }; 168 | DDE82E951E647EFD00B30065 /* NotesStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotesStorage.swift; path = Source/NotesStorage.swift; sourceTree = ""; }; 169 | DDE82E961E647EFD00B30065 /* NotesSyncEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotesSyncEngine.swift; path = Source/NotesSyncEngine.swift; sourceTree = ""; }; 170 | /* End PBXFileReference section */ 171 | 172 | /* Begin PBXFrameworksBuildPhase section */ 173 | DD0201051E4E583500C4D40D /* Frameworks */ = { 174 | isa = PBXFrameworksBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | DD0201241E4E593A00C4D40D /* RxSwift.framework in Frameworks */, 178 | DD0201221E4E593A00C4D40D /* RxCocoa.framework in Frameworks */, 179 | DD6980491E4FC5B200A22EDC /* RxOptional.framework in Frameworks */, 180 | DD1A3D0A1E4E99020068347D /* IGListKit.framework in Frameworks */, 181 | DD02013D1E4E8D2F00C4D40D /* CloudKit.framework in Frameworks */, 182 | DDE82E7F1E647E7800B30065 /* NoteTakerCore.framework in Frameworks */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | DDD6318F1E64974E00937A6A /* Frameworks */ = { 187 | isa = PBXFrameworksBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | DDD631A61E64976D00937A6A /* CloudKit.framework in Frameworks */, 191 | DDD631B01E64988300937A6A /* RxCocoa.framework in Frameworks */, 192 | DDD631B21E64988D00937A6A /* RxOptional.framework in Frameworks */, 193 | DDD631CA1E64995500937A6A /* NoteTakerCore.framework in Frameworks */, 194 | DDD631B11E64988600937A6A /* RxSwift.framework in Frameworks */, 195 | DDD631AF1E64987D00937A6A /* IGListKit.framework in Frameworks */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | DDD631BD1E6498F700937A6A /* Frameworks */ = { 200 | isa = PBXFrameworksBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | DDE82E741E647E7800B30065 /* Frameworks */ = { 207 | isa = PBXFrameworksBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | DDE82E9A1E647F1900B30065 /* Realm.framework in Frameworks */, 211 | DDE82E9B1E647F1B00B30065 /* RealmSwift.framework in Frameworks */, 212 | DDE82E9C1E647F1F00B30065 /* RxSwift.framework in Frameworks */, 213 | DDE82E9D1E647F2200B30065 /* IGListKit.framework in Frameworks */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXFrameworksBuildPhase section */ 218 | 219 | /* Begin PBXGroup section */ 220 | DD0200FF1E4E583500C4D40D = { 221 | isa = PBXGroup; 222 | children = ( 223 | DD0201311E4E59DC00C4D40D /* Vendor */, 224 | DD02010A1E4E583500C4D40D /* NoteTaker */, 225 | DDE82E791E647E7800B30065 /* NoteTakerCore */, 226 | DDD631931E64974E00937A6A /* MobileNoteTaker */, 227 | DD0201091E4E583500C4D40D /* Products */, 228 | DD02011A1E4E593A00C4D40D /* Frameworks */, 229 | ); 230 | sourceTree = ""; 231 | }; 232 | DD0201091E4E583500C4D40D /* Products */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | DD0201081E4E583500C4D40D /* NoteTaker.app */, 236 | DDE82E781E647E7800B30065 /* NoteTakerCore.framework */, 237 | DDD631921E64974E00937A6A /* MobileNoteTaker.app */, 238 | DDD631C81E6498F700937A6A /* NoteTakerCore.framework */, 239 | ); 240 | name = Products; 241 | sourceTree = ""; 242 | }; 243 | DD02010A1E4E583500C4D40D /* NoteTaker */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | DD02013B1E4E8D2700C4D40D /* NoteTaker.entitlements */, 247 | DD0201341E4E59F600C4D40D /* Bootstrap */, 248 | DD0201381E4E5A0C00C4D40D /* Resources */, 249 | DD0201351E4E59FA00C4D40D /* ViewControllers */, 250 | DD1A3D101E4EA1040068347D /* Util */, 251 | ); 252 | path = NoteTaker; 253 | sourceTree = ""; 254 | }; 255 | DD02011A1E4E593A00C4D40D /* Frameworks */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | DDD631A51E64976D00937A6A /* CloudKit.framework */, 259 | DD02013C1E4E8D2F00C4D40D /* CloudKit.framework */, 260 | ); 261 | name = Frameworks; 262 | sourceTree = ""; 263 | }; 264 | DD0201311E4E59DC00C4D40D /* Vendor */ = { 265 | isa = PBXGroup; 266 | children = ( 267 | DD0201321E4E59E000C4D40D /* Frameworks */, 268 | ); 269 | name = Vendor; 270 | sourceTree = ""; 271 | }; 272 | DD0201321E4E59E000C4D40D /* Frameworks */ = { 273 | isa = PBXGroup; 274 | children = ( 275 | DDE82EB51E6481B700B30065 /* macOS */, 276 | DDE82EB71E6481C900B30065 /* iOS */, 277 | ); 278 | name = Frameworks; 279 | sourceTree = ""; 280 | }; 281 | DD0201341E4E59F600C4D40D /* Bootstrap */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | DD02010B1E4E583500C4D40D /* AppDelegate.swift */, 285 | ); 286 | name = Bootstrap; 287 | sourceTree = ""; 288 | }; 289 | DD0201351E4E59FA00C4D40D /* ViewControllers */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | DD1A3D181E4EA4520068347D /* NeedsStorage.swift */, 293 | DD1A3D071E4E92910068347D /* NotesSplitViewController.swift */, 294 | DD02013E1E4E8EBC00C4D40D /* NotesTableViewController.swift */, 295 | DD0201401E4E8EC500C4D40D /* NoteEditorViewController.swift */, 296 | ); 297 | name = ViewControllers; 298 | sourceTree = ""; 299 | }; 300 | DD0201381E4E5A0C00C4D40D /* Resources */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | DD02010F1E4E583500C4D40D /* Assets.xcassets */, 304 | DD0201111E4E583500C4D40D /* Main.storyboard */, 305 | DD0201141E4E583500C4D40D /* Info.plist */, 306 | ); 307 | name = Resources; 308 | sourceTree = ""; 309 | }; 310 | DD1A3D101E4EA1040068347D /* Util */ = { 311 | isa = PBXGroup; 312 | children = ( 313 | DD1A3D111E4EA1110068347D /* NoteTaker-Bridging-Header.h */, 314 | DD1A3D121E4EA1110068347D /* NSTableView+Diff.h */, 315 | DD1A3D131E4EA1110068347D /* NSTableView+Diff.m */, 316 | DDCDA0AD1E58AE44002B63CB /* NSToolbarFlexibleSpaceItem.h */, 317 | DD6980461E4F4C9700A22EDC /* NSTableView+Rx.swift */, 318 | ); 319 | name = Util; 320 | sourceTree = ""; 321 | }; 322 | DDD631931E64974E00937A6A /* MobileNoteTaker */ = { 323 | isa = PBXGroup; 324 | children = ( 325 | DDD631A41E64976400937A6A /* MobileNoteTaker.entitlements */, 326 | DDD631D71E64A49100937A6A /* Bootstrap */, 327 | DDD631D91E64A49A00937A6A /* Controllers */, 328 | DDD631D81E64A49600937A6A /* Resources */, 329 | ); 330 | path = MobileNoteTaker; 331 | sourceTree = ""; 332 | }; 333 | DDD631CF1E649AD600937A6A /* Views */ = { 334 | isa = PBXGroup; 335 | children = ( 336 | DDD631D01E649B4100937A6A /* NoteEditorView.swift */, 337 | ); 338 | name = Views; 339 | sourceTree = ""; 340 | }; 341 | DDD631D21E649D7800937A6A /* Resources */ = { 342 | isa = PBXGroup; 343 | children = ( 344 | DDD631D31E649D7F00937A6A /* Editor.html */, 345 | ); 346 | name = Resources; 347 | sourceTree = ""; 348 | }; 349 | DDD631D71E64A49100937A6A /* Bootstrap */ = { 350 | isa = PBXGroup; 351 | children = ( 352 | DDD631941E64974E00937A6A /* AppDelegate.swift */, 353 | ); 354 | name = Bootstrap; 355 | sourceTree = ""; 356 | }; 357 | DDD631D81E64A49600937A6A /* Resources */ = { 358 | isa = PBXGroup; 359 | children = ( 360 | DDD6319B1E64974E00937A6A /* Assets.xcassets */, 361 | DDD6319D1E64974E00937A6A /* LaunchScreen.storyboard */, 362 | DDD631A01E64974E00937A6A /* Info.plist */, 363 | ); 364 | name = Resources; 365 | sourceTree = ""; 366 | }; 367 | DDD631D91E64A49A00937A6A /* Controllers */ = { 368 | isa = PBXGroup; 369 | children = ( 370 | DDD631961E64974E00937A6A /* NotesTableViewController.swift */, 371 | ); 372 | name = Controllers; 373 | sourceTree = ""; 374 | }; 375 | DDE82E791E647E7800B30065 /* NoteTakerCore */ = { 376 | isa = PBXGroup; 377 | children = ( 378 | DDD631D21E649D7800937A6A /* Resources */, 379 | DDE82E851E647EB400B30065 /* Source */, 380 | DDE82E7A1E647E7800B30065 /* NoteTakerCore.h */, 381 | DDE82E7B1E647E7800B30065 /* Info.plist */, 382 | ); 383 | path = NoteTakerCore; 384 | sourceTree = ""; 385 | }; 386 | DDE82E851E647EB400B30065 /* Source */ = { 387 | isa = PBXGroup; 388 | children = ( 389 | DDD631CF1E649AD600937A6A /* Views */, 390 | DDE82E881E647EBF00B30065 /* Storage and Sync */, 391 | DDE82E861E647EB800B30065 /* Models */, 392 | DDE82E871E647EBC00B30065 /* ViewModels */, 393 | ); 394 | name = Source; 395 | sourceTree = ""; 396 | }; 397 | DDE82E861E647EB800B30065 /* Models */ = { 398 | isa = PBXGroup; 399 | children = ( 400 | DDE82E891E647EDE00B30065 /* Note.swift */, 401 | DDE82E8A1E647EDE00B30065 /* Note+RealmNote.swift */, 402 | DDE82E8B1E647EDE00B30065 /* RealmNote.swift */, 403 | DDE82E8C1E647EDE00B30065 /* RealmNote+CKRecord.swift */, 404 | ); 405 | name = Models; 406 | sourceTree = ""; 407 | }; 408 | DDE82E871E647EBC00B30065 /* ViewModels */ = { 409 | isa = PBXGroup; 410 | children = ( 411 | DDE82E931E647EF300B30065 /* String+HTML.swift */, 412 | DDE82E911E647EEE00B30065 /* NoteViewModel.swift */, 413 | ); 414 | name = ViewModels; 415 | sourceTree = ""; 416 | }; 417 | DDE82E881E647EBF00B30065 /* Storage and Sync */ = { 418 | isa = PBXGroup; 419 | children = ( 420 | DDE82E951E647EFD00B30065 /* NotesStorage.swift */, 421 | DDE82E961E647EFD00B30065 /* NotesSyncEngine.swift */, 422 | ); 423 | name = "Storage and Sync"; 424 | sourceTree = ""; 425 | }; 426 | DDE82EB51E6481B700B30065 /* macOS */ = { 427 | isa = PBXGroup; 428 | children = ( 429 | DD6980481E4FC5B200A22EDC /* RxOptional.framework */, 430 | DD02011B1E4E593A00C4D40D /* Realm.framework */, 431 | DD02011C1E4E593A00C4D40D /* RealmSwift.framework */, 432 | DD02011D1E4E593A00C4D40D /* RxCocoa.framework */, 433 | DD02011E1E4E593A00C4D40D /* RxRealm.framework */, 434 | DD02011F1E4E593A00C4D40D /* RxSwift.framework */, 435 | DD1A3D091E4E99020068347D /* IGListKit.framework */, 436 | ); 437 | name = macOS; 438 | sourceTree = ""; 439 | }; 440 | DDE82EB71E6481C900B30065 /* iOS */ = { 441 | isa = PBXGroup; 442 | children = ( 443 | DDD631A81E64984900937A6A /* IGListKit.framework */, 444 | DDD631A91E64984900937A6A /* Realm.framework */, 445 | DDD631AA1E64984900937A6A /* RealmSwift.framework */, 446 | DDD631AB1E64984900937A6A /* RxCocoa.framework */, 447 | DDD631AC1E64984900937A6A /* RxOptional.framework */, 448 | DDD631AD1E64984900937A6A /* RxRealm.framework */, 449 | DDD631AE1E64984900937A6A /* RxSwift.framework */, 450 | ); 451 | name = iOS; 452 | sourceTree = ""; 453 | }; 454 | /* End PBXGroup section */ 455 | 456 | /* Begin PBXHeadersBuildPhase section */ 457 | DDD631C21E6498F700937A6A /* Headers */ = { 458 | isa = PBXHeadersBuildPhase; 459 | buildActionMask = 2147483647; 460 | files = ( 461 | DDD631C31E6498F700937A6A /* NoteTakerCore.h in Headers */, 462 | ); 463 | runOnlyForDeploymentPostprocessing = 0; 464 | }; 465 | DDE82E751E647E7800B30065 /* Headers */ = { 466 | isa = PBXHeadersBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | DDE82E7C1E647E7800B30065 /* NoteTakerCore.h in Headers */, 470 | ); 471 | runOnlyForDeploymentPostprocessing = 0; 472 | }; 473 | /* End PBXHeadersBuildPhase section */ 474 | 475 | /* Begin PBXNativeTarget section */ 476 | DD0201071E4E583500C4D40D /* NoteTaker */ = { 477 | isa = PBXNativeTarget; 478 | buildConfigurationList = DD0201171E4E583500C4D40D /* Build configuration list for PBXNativeTarget "NoteTaker" */; 479 | buildPhases = ( 480 | DD0201041E4E583500C4D40D /* Sources */, 481 | DD0201051E4E583500C4D40D /* Frameworks */, 482 | DD0201061E4E583500C4D40D /* Resources */, 483 | DD0201251E4E594000C4D40D /* Carthage: Copy Frameworks */, 484 | DD0201261E4E59B900C4D40D /* Carthage: Copy DSYMs */, 485 | DDE82E801E647E7800B30065 /* Embed Frameworks */, 486 | ); 487 | buildRules = ( 488 | ); 489 | dependencies = ( 490 | DDE82E7E1E647E7800B30065 /* PBXTargetDependency */, 491 | ); 492 | name = NoteTaker; 493 | productName = NoteTaker; 494 | productReference = DD0201081E4E583500C4D40D /* NoteTaker.app */; 495 | productType = "com.apple.product-type.application"; 496 | }; 497 | DDD631911E64974E00937A6A /* MobileNoteTaker */ = { 498 | isa = PBXNativeTarget; 499 | buildConfigurationList = DDD631A31E64974E00937A6A /* Build configuration list for PBXNativeTarget "MobileNoteTaker" */; 500 | buildPhases = ( 501 | DDD6318E1E64974E00937A6A /* Sources */, 502 | DDD6318F1E64974E00937A6A /* Frameworks */, 503 | DDD631901E64974E00937A6A /* Resources */, 504 | DDD631A71E6497A500937A6A /* Carthage: Copy Frameworks */, 505 | DDD631CE1E64995500937A6A /* Embed Frameworks */, 506 | ); 507 | buildRules = ( 508 | ); 509 | dependencies = ( 510 | DDD631CD1E64995500937A6A /* PBXTargetDependency */, 511 | ); 512 | name = MobileNoteTaker; 513 | productName = MobileNoteTaker; 514 | productReference = DDD631921E64974E00937A6A /* MobileNoteTaker.app */; 515 | productType = "com.apple.product-type.application"; 516 | }; 517 | DDD631B31E6498F700937A6A /* NoteTakerCore-iOS */ = { 518 | isa = PBXNativeTarget; 519 | buildConfigurationList = DDD631C51E6498F700937A6A /* Build configuration list for PBXNativeTarget "NoteTakerCore-iOS" */; 520 | buildPhases = ( 521 | DDD631B41E6498F700937A6A /* Sources */, 522 | DDD631BD1E6498F700937A6A /* Frameworks */, 523 | DDD631C21E6498F700937A6A /* Headers */, 524 | DDD631C41E6498F700937A6A /* Resources */, 525 | ); 526 | buildRules = ( 527 | ); 528 | dependencies = ( 529 | ); 530 | name = "NoteTakerCore-iOS"; 531 | productName = NoteTakerCore; 532 | productReference = DDD631C81E6498F700937A6A /* NoteTakerCore.framework */; 533 | productType = "com.apple.product-type.framework"; 534 | }; 535 | DDE82E771E647E7800B30065 /* NoteTakerCore */ = { 536 | isa = PBXNativeTarget; 537 | buildConfigurationList = DDE82E841E647E7800B30065 /* Build configuration list for PBXNativeTarget "NoteTakerCore" */; 538 | buildPhases = ( 539 | DDE82E731E647E7800B30065 /* Sources */, 540 | DDE82E741E647E7800B30065 /* Frameworks */, 541 | DDE82E751E647E7800B30065 /* Headers */, 542 | DDE82E761E647E7800B30065 /* Resources */, 543 | ); 544 | buildRules = ( 545 | ); 546 | dependencies = ( 547 | ); 548 | name = NoteTakerCore; 549 | productName = NoteTakerCore; 550 | productReference = DDE82E781E647E7800B30065 /* NoteTakerCore.framework */; 551 | productType = "com.apple.product-type.framework"; 552 | }; 553 | /* End PBXNativeTarget section */ 554 | 555 | /* Begin PBXProject section */ 556 | DD0201001E4E583500C4D40D /* Project object */ = { 557 | isa = PBXProject; 558 | attributes = { 559 | LastSwiftUpdateCheck = 0820; 560 | LastUpgradeCheck = 0830; 561 | ORGANIZATIONNAME = "Guilherme Rambo"; 562 | TargetAttributes = { 563 | DD0201071E4E583500C4D40D = { 564 | CreatedOnToolsVersion = 8.2.1; 565 | DevelopmentTeam = 8C7439RJLG; 566 | LastSwiftMigration = 0820; 567 | ProvisioningStyle = Automatic; 568 | SystemCapabilities = { 569 | com.apple.Push = { 570 | enabled = 1; 571 | }; 572 | com.apple.iCloud = { 573 | enabled = 1; 574 | }; 575 | }; 576 | }; 577 | DDD631911E64974E00937A6A = { 578 | CreatedOnToolsVersion = 8.2.1; 579 | DevelopmentTeam = 8C7439RJLG; 580 | ProvisioningStyle = Automatic; 581 | SystemCapabilities = { 582 | com.apple.BackgroundModes = { 583 | enabled = 1; 584 | }; 585 | com.apple.Push = { 586 | enabled = 1; 587 | }; 588 | com.apple.iCloud = { 589 | enabled = 1; 590 | }; 591 | }; 592 | }; 593 | DDD631B31E6498F700937A6A = { 594 | DevelopmentTeam = 8C7439RJLG; 595 | }; 596 | DDE82E771E647E7800B30065 = { 597 | CreatedOnToolsVersion = 8.2.1; 598 | DevelopmentTeam = 8C7439RJLG; 599 | LastSwiftMigration = 0820; 600 | ProvisioningStyle = Automatic; 601 | }; 602 | }; 603 | }; 604 | buildConfigurationList = DD0201031E4E583500C4D40D /* Build configuration list for PBXProject "NoteTaker" */; 605 | compatibilityVersion = "Xcode 3.2"; 606 | developmentRegion = English; 607 | hasScannedForEncodings = 0; 608 | knownRegions = ( 609 | en, 610 | Base, 611 | ); 612 | mainGroup = DD0200FF1E4E583500C4D40D; 613 | productRefGroup = DD0201091E4E583500C4D40D /* Products */; 614 | projectDirPath = ""; 615 | projectRoot = ""; 616 | targets = ( 617 | DD0201071E4E583500C4D40D /* NoteTaker */, 618 | DDD631911E64974E00937A6A /* MobileNoteTaker */, 619 | DDE82E771E647E7800B30065 /* NoteTakerCore */, 620 | DDD631B31E6498F700937A6A /* NoteTakerCore-iOS */, 621 | ); 622 | }; 623 | /* End PBXProject section */ 624 | 625 | /* Begin PBXResourcesBuildPhase section */ 626 | DD0201061E4E583500C4D40D /* Resources */ = { 627 | isa = PBXResourcesBuildPhase; 628 | buildActionMask = 2147483647; 629 | files = ( 630 | DD0201101E4E583500C4D40D /* Assets.xcassets in Resources */, 631 | DD0201131E4E583500C4D40D /* Main.storyboard in Resources */, 632 | ); 633 | runOnlyForDeploymentPostprocessing = 0; 634 | }; 635 | DDD631901E64974E00937A6A /* Resources */ = { 636 | isa = PBXResourcesBuildPhase; 637 | buildActionMask = 2147483647; 638 | files = ( 639 | DDD6319F1E64974E00937A6A /* LaunchScreen.storyboard in Resources */, 640 | DDD6319C1E64974E00937A6A /* Assets.xcassets in Resources */, 641 | ); 642 | runOnlyForDeploymentPostprocessing = 0; 643 | }; 644 | DDD631C41E6498F700937A6A /* Resources */ = { 645 | isa = PBXResourcesBuildPhase; 646 | buildActionMask = 2147483647; 647 | files = ( 648 | DDD631D51E649D7F00937A6A /* Editor.html in Resources */, 649 | ); 650 | runOnlyForDeploymentPostprocessing = 0; 651 | }; 652 | DDE82E761E647E7800B30065 /* Resources */ = { 653 | isa = PBXResourcesBuildPhase; 654 | buildActionMask = 2147483647; 655 | files = ( 656 | DDD631D41E649D7F00937A6A /* Editor.html in Resources */, 657 | ); 658 | runOnlyForDeploymentPostprocessing = 0; 659 | }; 660 | /* End PBXResourcesBuildPhase section */ 661 | 662 | /* Begin PBXShellScriptBuildPhase section */ 663 | DD0201251E4E594000C4D40D /* Carthage: Copy Frameworks */ = { 664 | isa = PBXShellScriptBuildPhase; 665 | buildActionMask = 2147483647; 666 | files = ( 667 | ); 668 | inputPaths = ( 669 | "$(SRCROOT)/Carthage/Build/Mac/Realm.framework", 670 | "$(SRCROOT)/Carthage/Build/Mac/RealmSwift.framework", 671 | "$(SRCROOT)/Carthage/Build/Mac/RxRealm.framework", 672 | "$(SRCROOT)/Carthage/Build/Mac/RxSwift.framework", 673 | "$(SRCROOT)/Carthage/Build/Mac/RxCocoa.framework", 674 | "$(SRCROOT)/Carthage/Build/Mac/IGListKit.framework", 675 | "$(SRCROOT)/Carthage/Build/Mac/RxOptional.framework", 676 | ); 677 | name = "Carthage: Copy Frameworks"; 678 | outputPaths = ( 679 | ); 680 | runOnlyForDeploymentPostprocessing = 0; 681 | shellPath = /bin/sh; 682 | shellScript = "/usr/local/bin/carthage copy-frameworks"; 683 | }; 684 | DDD631A71E6497A500937A6A /* Carthage: Copy Frameworks */ = { 685 | isa = PBXShellScriptBuildPhase; 686 | buildActionMask = 2147483647; 687 | files = ( 688 | ); 689 | inputPaths = ( 690 | "$(SRCROOT)/Carthage/Build/iOS/Realm.framework", 691 | "$(SRCROOT)/Carthage/Build/iOS/RealmSwift.framework", 692 | "$(SRCROOT)/Carthage/Build/iOS/RxRealm.framework", 693 | "$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework", 694 | "$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework", 695 | "$(SRCROOT)/Carthage/Build/iOS/IGListKit.framework", 696 | "$(SRCROOT)/Carthage/Build/iOS/RxOptional.framework", 697 | ); 698 | name = "Carthage: Copy Frameworks"; 699 | outputPaths = ( 700 | ); 701 | runOnlyForDeploymentPostprocessing = 0; 702 | shellPath = /bin/sh; 703 | shellScript = "/usr/local/bin/carthage copy-frameworks"; 704 | }; 705 | /* End PBXShellScriptBuildPhase section */ 706 | 707 | /* Begin PBXSourcesBuildPhase section */ 708 | DD0201041E4E583500C4D40D /* Sources */ = { 709 | isa = PBXSourcesBuildPhase; 710 | buildActionMask = 2147483647; 711 | files = ( 712 | DD02010C1E4E583500C4D40D /* AppDelegate.swift in Sources */, 713 | DD1A3D191E4EA4520068347D /* NeedsStorage.swift in Sources */, 714 | DD0201421E4E8EC500C4D40D /* NoteEditorViewController.swift in Sources */, 715 | DD1A3D141E4EA1110068347D /* NSTableView+Diff.m in Sources */, 716 | DD6980471E4F4C9700A22EDC /* NSTableView+Rx.swift in Sources */, 717 | DD1A3D081E4E92910068347D /* NotesSplitViewController.swift in Sources */, 718 | DD02013F1E4E8EBC00C4D40D /* NotesTableViewController.swift in Sources */, 719 | ); 720 | runOnlyForDeploymentPostprocessing = 0; 721 | }; 722 | DDD6318E1E64974E00937A6A /* Sources */ = { 723 | isa = PBXSourcesBuildPhase; 724 | buildActionMask = 2147483647; 725 | files = ( 726 | DDD631971E64974E00937A6A /* NotesTableViewController.swift in Sources */, 727 | DDD631951E64974E00937A6A /* AppDelegate.swift in Sources */, 728 | ); 729 | runOnlyForDeploymentPostprocessing = 0; 730 | }; 731 | DDD631B41E6498F700937A6A /* Sources */ = { 732 | isa = PBXSourcesBuildPhase; 733 | buildActionMask = 2147483647; 734 | files = ( 735 | DDD631B51E6498F700937A6A /* NotesStorage.swift in Sources */, 736 | DDD631B61E6498F700937A6A /* NoteViewModel.swift in Sources */, 737 | DDD631D61E64A38B00937A6A /* NoteEditorView.swift in Sources */, 738 | DDD631B71E6498F700937A6A /* Note.swift in Sources */, 739 | DDD631B81E6498F700937A6A /* Note+RealmNote.swift in Sources */, 740 | DDD631B91E6498F700937A6A /* NotesSyncEngine.swift in Sources */, 741 | DDD631BA1E6498F700937A6A /* String+HTML.swift in Sources */, 742 | DDD631BB1E6498F700937A6A /* RealmNote+CKRecord.swift in Sources */, 743 | DDD631BC1E6498F700937A6A /* RealmNote.swift in Sources */, 744 | ); 745 | runOnlyForDeploymentPostprocessing = 0; 746 | }; 747 | DDE82E731E647E7800B30065 /* Sources */ = { 748 | isa = PBXSourcesBuildPhase; 749 | buildActionMask = 2147483647; 750 | files = ( 751 | DDE82E971E647EFD00B30065 /* NotesStorage.swift in Sources */, 752 | DDE82E921E647EEE00B30065 /* NoteViewModel.swift in Sources */, 753 | DDD631D11E649B4100937A6A /* NoteEditorView.swift in Sources */, 754 | DDE82E8D1E647EDE00B30065 /* Note.swift in Sources */, 755 | DDE82E8E1E647EDE00B30065 /* Note+RealmNote.swift in Sources */, 756 | DDE82E981E647EFD00B30065 /* NotesSyncEngine.swift in Sources */, 757 | DDE82E941E647EF300B30065 /* String+HTML.swift in Sources */, 758 | DDE82E901E647EDE00B30065 /* RealmNote+CKRecord.swift in Sources */, 759 | DDE82E8F1E647EDE00B30065 /* RealmNote.swift in Sources */, 760 | ); 761 | runOnlyForDeploymentPostprocessing = 0; 762 | }; 763 | /* End PBXSourcesBuildPhase section */ 764 | 765 | /* Begin PBXTargetDependency section */ 766 | DDD631CD1E64995500937A6A /* PBXTargetDependency */ = { 767 | isa = PBXTargetDependency; 768 | target = DDD631B31E6498F700937A6A /* NoteTakerCore-iOS */; 769 | targetProxy = DDD631CC1E64995500937A6A /* PBXContainerItemProxy */; 770 | }; 771 | DDE82E7E1E647E7800B30065 /* PBXTargetDependency */ = { 772 | isa = PBXTargetDependency; 773 | target = DDE82E771E647E7800B30065 /* NoteTakerCore */; 774 | targetProxy = DDE82E7D1E647E7800B30065 /* PBXContainerItemProxy */; 775 | }; 776 | /* End PBXTargetDependency section */ 777 | 778 | /* Begin PBXVariantGroup section */ 779 | DD0201111E4E583500C4D40D /* Main.storyboard */ = { 780 | isa = PBXVariantGroup; 781 | children = ( 782 | DD0201121E4E583500C4D40D /* Base */, 783 | ); 784 | name = Main.storyboard; 785 | sourceTree = ""; 786 | }; 787 | DDD6319D1E64974E00937A6A /* LaunchScreen.storyboard */ = { 788 | isa = PBXVariantGroup; 789 | children = ( 790 | DDD6319E1E64974E00937A6A /* Base */, 791 | ); 792 | name = LaunchScreen.storyboard; 793 | sourceTree = ""; 794 | }; 795 | /* End PBXVariantGroup section */ 796 | 797 | /* Begin XCBuildConfiguration section */ 798 | DD0201151E4E583500C4D40D /* Debug */ = { 799 | isa = XCBuildConfiguration; 800 | buildSettings = { 801 | ALWAYS_SEARCH_USER_PATHS = NO; 802 | CLANG_ANALYZER_NONNULL = YES; 803 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 804 | CLANG_CXX_LIBRARY = "libc++"; 805 | CLANG_ENABLE_MODULES = YES; 806 | CLANG_ENABLE_OBJC_ARC = YES; 807 | CLANG_WARN_BOOL_CONVERSION = YES; 808 | CLANG_WARN_CONSTANT_CONVERSION = YES; 809 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 810 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 811 | CLANG_WARN_EMPTY_BODY = YES; 812 | CLANG_WARN_ENUM_CONVERSION = YES; 813 | CLANG_WARN_INFINITE_RECURSION = YES; 814 | CLANG_WARN_INT_CONVERSION = YES; 815 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 816 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 817 | CLANG_WARN_UNREACHABLE_CODE = YES; 818 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 819 | CODE_SIGN_IDENTITY = "-"; 820 | COPY_PHASE_STRIP = NO; 821 | DEBUG_INFORMATION_FORMAT = dwarf; 822 | ENABLE_STRICT_OBJC_MSGSEND = YES; 823 | ENABLE_TESTABILITY = YES; 824 | GCC_C_LANGUAGE_STANDARD = gnu99; 825 | GCC_DYNAMIC_NO_PIC = NO; 826 | GCC_NO_COMMON_BLOCKS = YES; 827 | GCC_OPTIMIZATION_LEVEL = 0; 828 | GCC_PREPROCESSOR_DEFINITIONS = ( 829 | "DEBUG=1", 830 | "$(inherited)", 831 | ); 832 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 833 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 834 | GCC_WARN_UNDECLARED_SELECTOR = YES; 835 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 836 | GCC_WARN_UNUSED_FUNCTION = YES; 837 | GCC_WARN_UNUSED_VARIABLE = YES; 838 | MACOSX_DEPLOYMENT_TARGET = 10.12; 839 | MTL_ENABLE_DEBUG_INFO = YES; 840 | ONLY_ACTIVE_ARCH = YES; 841 | SDKROOT = macosx; 842 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 843 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 844 | }; 845 | name = Debug; 846 | }; 847 | DD0201161E4E583500C4D40D /* Release */ = { 848 | isa = XCBuildConfiguration; 849 | buildSettings = { 850 | ALWAYS_SEARCH_USER_PATHS = NO; 851 | CLANG_ANALYZER_NONNULL = YES; 852 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 853 | CLANG_CXX_LIBRARY = "libc++"; 854 | CLANG_ENABLE_MODULES = YES; 855 | CLANG_ENABLE_OBJC_ARC = YES; 856 | CLANG_WARN_BOOL_CONVERSION = YES; 857 | CLANG_WARN_CONSTANT_CONVERSION = YES; 858 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 859 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 860 | CLANG_WARN_EMPTY_BODY = YES; 861 | CLANG_WARN_ENUM_CONVERSION = YES; 862 | CLANG_WARN_INFINITE_RECURSION = YES; 863 | CLANG_WARN_INT_CONVERSION = YES; 864 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 865 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 866 | CLANG_WARN_UNREACHABLE_CODE = YES; 867 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 868 | CODE_SIGN_IDENTITY = "-"; 869 | COPY_PHASE_STRIP = NO; 870 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 871 | ENABLE_NS_ASSERTIONS = NO; 872 | ENABLE_STRICT_OBJC_MSGSEND = YES; 873 | GCC_C_LANGUAGE_STANDARD = gnu99; 874 | GCC_NO_COMMON_BLOCKS = YES; 875 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 876 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 877 | GCC_WARN_UNDECLARED_SELECTOR = YES; 878 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 879 | GCC_WARN_UNUSED_FUNCTION = YES; 880 | GCC_WARN_UNUSED_VARIABLE = YES; 881 | MACOSX_DEPLOYMENT_TARGET = 10.12; 882 | MTL_ENABLE_DEBUG_INFO = NO; 883 | SDKROOT = macosx; 884 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 885 | }; 886 | name = Release; 887 | }; 888 | DD0201181E4E583500C4D40D /* Debug */ = { 889 | isa = XCBuildConfiguration; 890 | buildSettings = { 891 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 892 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 893 | CLANG_ENABLE_MODULES = YES; 894 | CODE_SIGN_ENTITLEMENTS = NoteTaker/NoteTaker.entitlements; 895 | CODE_SIGN_IDENTITY = "Mac Developer"; 896 | COMBINE_HIDPI_IMAGES = YES; 897 | DEVELOPMENT_TEAM = 8C7439RJLG; 898 | FRAMEWORK_SEARCH_PATHS = ( 899 | "$(inherited)", 900 | "$(PROJECT_DIR)/Carthage/Build/Mac", 901 | ); 902 | INFOPLIST_FILE = NoteTaker/Info.plist; 903 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 904 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.NoteTaker; 905 | PRODUCT_NAME = "$(TARGET_NAME)"; 906 | SWIFT_OBJC_BRIDGING_HEADER = "NoteTaker/NoteTaker-Bridging-Header.h"; 907 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 908 | SWIFT_VERSION = 3.0; 909 | }; 910 | name = Debug; 911 | }; 912 | DD0201191E4E583500C4D40D /* Release */ = { 913 | isa = XCBuildConfiguration; 914 | buildSettings = { 915 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 916 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 917 | CLANG_ENABLE_MODULES = YES; 918 | CODE_SIGN_ENTITLEMENTS = NoteTaker/NoteTaker.entitlements; 919 | CODE_SIGN_IDENTITY = "Mac Developer"; 920 | COMBINE_HIDPI_IMAGES = YES; 921 | DEVELOPMENT_TEAM = 8C7439RJLG; 922 | FRAMEWORK_SEARCH_PATHS = ( 923 | "$(inherited)", 924 | "$(PROJECT_DIR)/Carthage/Build/Mac", 925 | ); 926 | INFOPLIST_FILE = NoteTaker/Info.plist; 927 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 928 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.NoteTaker; 929 | PRODUCT_NAME = "$(TARGET_NAME)"; 930 | SWIFT_OBJC_BRIDGING_HEADER = "NoteTaker/NoteTaker-Bridging-Header.h"; 931 | SWIFT_VERSION = 3.0; 932 | }; 933 | name = Release; 934 | }; 935 | DDD631A11E64974E00937A6A /* Debug */ = { 936 | isa = XCBuildConfiguration; 937 | buildSettings = { 938 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 939 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 940 | CODE_SIGN_ENTITLEMENTS = MobileNoteTaker/MobileNoteTaker.entitlements; 941 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 942 | DEVELOPMENT_TEAM = 8C7439RJLG; 943 | FRAMEWORK_SEARCH_PATHS = ( 944 | "$(inherited)", 945 | "$(PROJECT_DIR)/Carthage/Build/iOS", 946 | ); 947 | INFOPLIST_FILE = MobileNoteTaker/Info.plist; 948 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 949 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 950 | OTHER_SWIFT_FLAGS = "-D DEBUG"; 951 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.MobileNoteTaker; 952 | PRODUCT_NAME = "$(TARGET_NAME)"; 953 | SDKROOT = iphoneos; 954 | SWIFT_VERSION = 3.0; 955 | TARGETED_DEVICE_FAMILY = "1,2"; 956 | }; 957 | name = Debug; 958 | }; 959 | DDD631A21E64974E00937A6A /* Release */ = { 960 | isa = XCBuildConfiguration; 961 | buildSettings = { 962 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 963 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 964 | CODE_SIGN_ENTITLEMENTS = MobileNoteTaker/MobileNoteTaker.entitlements; 965 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 966 | DEVELOPMENT_TEAM = 8C7439RJLG; 967 | FRAMEWORK_SEARCH_PATHS = ( 968 | "$(inherited)", 969 | "$(PROJECT_DIR)/Carthage/Build/iOS", 970 | ); 971 | INFOPLIST_FILE = MobileNoteTaker/Info.plist; 972 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 973 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 974 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.MobileNoteTaker; 975 | PRODUCT_NAME = "$(TARGET_NAME)"; 976 | SDKROOT = iphoneos; 977 | SWIFT_VERSION = 3.0; 978 | TARGETED_DEVICE_FAMILY = "1,2"; 979 | VALIDATE_PRODUCT = YES; 980 | }; 981 | name = Release; 982 | }; 983 | DDD631C61E6498F700937A6A /* Debug */ = { 984 | isa = XCBuildConfiguration; 985 | buildSettings = { 986 | CLANG_ENABLE_MODULES = YES; 987 | CODE_SIGN_IDENTITY = "-"; 988 | COMBINE_HIDPI_IMAGES = YES; 989 | CURRENT_PROJECT_VERSION = 1; 990 | DEFINES_MODULE = YES; 991 | DEVELOPMENT_TEAM = 8C7439RJLG; 992 | DYLIB_COMPATIBILITY_VERSION = 1; 993 | DYLIB_CURRENT_VERSION = 1; 994 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 995 | FRAMEWORK_SEARCH_PATHS = ( 996 | "$(inherited)", 997 | "$(PROJECT_DIR)/Carthage/Build/iOS", 998 | ); 999 | FRAMEWORK_VERSION = A; 1000 | INFOPLIST_FILE = NoteTakerCore/Info.plist; 1001 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 1002 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 1003 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.NoteTakerCore; 1004 | PRODUCT_NAME = NoteTakerCore; 1005 | SDKROOT = iphoneos; 1006 | SKIP_INSTALL = YES; 1007 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1008 | SWIFT_VERSION = 3.0; 1009 | VERSIONING_SYSTEM = "apple-generic"; 1010 | VERSION_INFO_PREFIX = ""; 1011 | }; 1012 | name = Debug; 1013 | }; 1014 | DDD631C71E6498F700937A6A /* Release */ = { 1015 | isa = XCBuildConfiguration; 1016 | buildSettings = { 1017 | CLANG_ENABLE_MODULES = YES; 1018 | CODE_SIGN_IDENTITY = "-"; 1019 | COMBINE_HIDPI_IMAGES = YES; 1020 | CURRENT_PROJECT_VERSION = 1; 1021 | DEFINES_MODULE = YES; 1022 | DEVELOPMENT_TEAM = 8C7439RJLG; 1023 | DYLIB_COMPATIBILITY_VERSION = 1; 1024 | DYLIB_CURRENT_VERSION = 1; 1025 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 1026 | FRAMEWORK_SEARCH_PATHS = ( 1027 | "$(inherited)", 1028 | "$(PROJECT_DIR)/Carthage/Build/iOS", 1029 | ); 1030 | FRAMEWORK_VERSION = A; 1031 | INFOPLIST_FILE = NoteTakerCore/Info.plist; 1032 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 1033 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 1034 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.NoteTakerCore; 1035 | PRODUCT_NAME = NoteTakerCore; 1036 | SDKROOT = iphoneos; 1037 | SKIP_INSTALL = YES; 1038 | SWIFT_VERSION = 3.0; 1039 | VERSIONING_SYSTEM = "apple-generic"; 1040 | VERSION_INFO_PREFIX = ""; 1041 | }; 1042 | name = Release; 1043 | }; 1044 | DDE82E821E647E7800B30065 /* Debug */ = { 1045 | isa = XCBuildConfiguration; 1046 | buildSettings = { 1047 | CLANG_ENABLE_MODULES = YES; 1048 | CODE_SIGN_IDENTITY = ""; 1049 | COMBINE_HIDPI_IMAGES = YES; 1050 | CURRENT_PROJECT_VERSION = 1; 1051 | DEFINES_MODULE = YES; 1052 | DEVELOPMENT_TEAM = 8C7439RJLG; 1053 | DYLIB_COMPATIBILITY_VERSION = 1; 1054 | DYLIB_CURRENT_VERSION = 1; 1055 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 1056 | FRAMEWORK_SEARCH_PATHS = ( 1057 | "$(inherited)", 1058 | "$(PROJECT_DIR)/Carthage/Build/Mac", 1059 | ); 1060 | FRAMEWORK_VERSION = A; 1061 | INFOPLIST_FILE = NoteTakerCore/Info.plist; 1062 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 1063 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 1064 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.NoteTakerCore; 1065 | PRODUCT_NAME = "$(TARGET_NAME)"; 1066 | SKIP_INSTALL = YES; 1067 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 1068 | SWIFT_VERSION = 3.0; 1069 | VERSIONING_SYSTEM = "apple-generic"; 1070 | VERSION_INFO_PREFIX = ""; 1071 | }; 1072 | name = Debug; 1073 | }; 1074 | DDE82E831E647E7800B30065 /* Release */ = { 1075 | isa = XCBuildConfiguration; 1076 | buildSettings = { 1077 | CLANG_ENABLE_MODULES = YES; 1078 | CODE_SIGN_IDENTITY = ""; 1079 | COMBINE_HIDPI_IMAGES = YES; 1080 | CURRENT_PROJECT_VERSION = 1; 1081 | DEFINES_MODULE = YES; 1082 | DEVELOPMENT_TEAM = 8C7439RJLG; 1083 | DYLIB_COMPATIBILITY_VERSION = 1; 1084 | DYLIB_CURRENT_VERSION = 1; 1085 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 1086 | FRAMEWORK_SEARCH_PATHS = ( 1087 | "$(inherited)", 1088 | "$(PROJECT_DIR)/Carthage/Build/Mac", 1089 | ); 1090 | FRAMEWORK_VERSION = A; 1091 | INFOPLIST_FILE = NoteTakerCore/Info.plist; 1092 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 1093 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 1094 | PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.NoteTakerCore; 1095 | PRODUCT_NAME = "$(TARGET_NAME)"; 1096 | SKIP_INSTALL = YES; 1097 | SWIFT_VERSION = 3.0; 1098 | VERSIONING_SYSTEM = "apple-generic"; 1099 | VERSION_INFO_PREFIX = ""; 1100 | }; 1101 | name = Release; 1102 | }; 1103 | /* End XCBuildConfiguration section */ 1104 | 1105 | /* Begin XCConfigurationList section */ 1106 | DD0201031E4E583500C4D40D /* Build configuration list for PBXProject "NoteTaker" */ = { 1107 | isa = XCConfigurationList; 1108 | buildConfigurations = ( 1109 | DD0201151E4E583500C4D40D /* Debug */, 1110 | DD0201161E4E583500C4D40D /* Release */, 1111 | ); 1112 | defaultConfigurationIsVisible = 0; 1113 | defaultConfigurationName = Release; 1114 | }; 1115 | DD0201171E4E583500C4D40D /* Build configuration list for PBXNativeTarget "NoteTaker" */ = { 1116 | isa = XCConfigurationList; 1117 | buildConfigurations = ( 1118 | DD0201181E4E583500C4D40D /* Debug */, 1119 | DD0201191E4E583500C4D40D /* Release */, 1120 | ); 1121 | defaultConfigurationIsVisible = 0; 1122 | defaultConfigurationName = Release; 1123 | }; 1124 | DDD631A31E64974E00937A6A /* Build configuration list for PBXNativeTarget "MobileNoteTaker" */ = { 1125 | isa = XCConfigurationList; 1126 | buildConfigurations = ( 1127 | DDD631A11E64974E00937A6A /* Debug */, 1128 | DDD631A21E64974E00937A6A /* Release */, 1129 | ); 1130 | defaultConfigurationIsVisible = 0; 1131 | defaultConfigurationName = Release; 1132 | }; 1133 | DDD631C51E6498F700937A6A /* Build configuration list for PBXNativeTarget "NoteTakerCore-iOS" */ = { 1134 | isa = XCConfigurationList; 1135 | buildConfigurations = ( 1136 | DDD631C61E6498F700937A6A /* Debug */, 1137 | DDD631C71E6498F700937A6A /* Release */, 1138 | ); 1139 | defaultConfigurationIsVisible = 0; 1140 | defaultConfigurationName = Release; 1141 | }; 1142 | DDE82E841E647E7800B30065 /* Build configuration list for PBXNativeTarget "NoteTakerCore" */ = { 1143 | isa = XCConfigurationList; 1144 | buildConfigurations = ( 1145 | DDE82E821E647E7800B30065 /* Debug */, 1146 | DDE82E831E647E7800B30065 /* Release */, 1147 | ); 1148 | defaultConfigurationIsVisible = 0; 1149 | defaultConfigurationName = Release; 1150 | }; 1151 | /* End XCConfigurationList section */ 1152 | }; 1153 | rootObject = DD0201001E4E583500C4D40D /* Project object */; 1154 | } 1155 | -------------------------------------------------------------------------------- /NoteTaker.xcodeproj/xcshareddata/xcschemes/NoteTakerCore.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /NoteTaker/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import NoteTakerCore 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | NSApp.registerForRemoteNotifications(matching: NSRemoteNotificationType()) 17 | } 18 | 19 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 20 | return true 21 | } 22 | 23 | func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { 24 | NotificationCenter.default.post(name: .notesDidChangeRemotely, object: nil, userInfo: userInfo) 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /NoteTaker/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /NoteTaker/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NoteTaker/Assets.xcassets/compose.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "compose@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /NoteTaker/Assets.xcassets/compose.imageset/compose@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/NoteTaker/db7e5f1fe60c18b4f27209d7a0889b1f199a383b/NoteTaker/Assets.xcassets/compose.imageset/compose@2x.png -------------------------------------------------------------------------------- /NoteTaker/Assets.xcassets/share.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "share@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /NoteTaker/Assets.xcassets/share.imageset/share@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/NoteTaker/db7e5f1fe60c18b4f27209d7a0889b1f199a383b/NoteTaker/Assets.xcassets/share.imageset/share@2x.png -------------------------------------------------------------------------------- /NoteTaker/Base.lproj/Main.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 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | CA 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | Default 514 | 515 | 516 | 517 | 518 | 519 | 520 | Left to Right 521 | 522 | 523 | 524 | 525 | 526 | 527 | Right to Left 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | Default 539 | 540 | 541 | 542 | 543 | 544 | 545 | Left to Right 546 | 547 | 548 | 549 | 550 | 551 | 552 | Right to Left 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 828 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | -------------------------------------------------------------------------------- /NoteTaker/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2017 Guilherme Rambo. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | CKSharingSupported 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /NoteTaker/NSTableView+Diff.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSTableView+Diff.h 3 | // Dose 4 | // 5 | // Created by Guilherme Rambo on 09/01/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | @import Cocoa; 10 | @import IGListKit; 11 | 12 | @interface NSTableView (Diff) 13 | 14 | - (void)reloadDataWithOldValue:(NSArray > *)oldValue newValue:(NSArray > *)newValue; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /NoteTaker/NSTableView+Diff.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSTableView+Diff.m 3 | // Dose 4 | // 5 | // Created by Guilherme Rambo on 09/01/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | #import "NSTableView+Diff.h" 10 | 11 | @implementation NSTableView (Diff) 12 | 13 | - (void)reloadDataWithOldValue:(NSArray > *)oldValue newValue:(NSArray > *)newValue 14 | { 15 | IGListIndexSetResult *result = IGListDiffWithBehavior(oldValue, newValue, IGListDiffEquality, IGListDiffBehaviorIncrementalMoves); 16 | 17 | [self beginUpdates]; 18 | { 19 | [self reloadDataForRowIndexes:result.updates columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tableColumns.count)]]; 20 | [self insertRowsAtIndexes:result.inserts withAnimation:NSTableViewAnimationSlideDown]; 21 | [self removeRowsAtIndexes:result.deletes withAnimation:NSTableViewAnimationSlideUp]; 22 | [result.moves enumerateObjectsUsingBlock:^(IGListMoveIndex *index, NSUInteger idx, BOOL *stop) { 23 | [self moveRowAtIndex:index.from toIndex:index.to]; 24 | }]; 25 | } 26 | [self endUpdates]; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /NoteTaker/NSTableView+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTableView+Rx.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 11/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class RxTableViewDelegateProxy: DelegateProxy, NSTableViewDelegate, DelegateProxyType { 14 | 15 | weak private(set) var tableView: NSTableView? 16 | 17 | fileprivate var selectedRowSubject = PublishSubject() 18 | 19 | required init(parentObject: AnyObject) { 20 | self.tableView = parentObject as? NSTableView 21 | 22 | super.init(parentObject: parentObject) 23 | } 24 | 25 | public override class func createProxyForObject(_ object: AnyObject) -> AnyObject { 26 | let control: NSTableView = object as! NSTableView 27 | return control.createRxDelegateProxy() 28 | } 29 | 30 | public class func currentDelegateFor(_ object: AnyObject) -> AnyObject? { 31 | let tableView: NSTableView = object as! NSTableView 32 | return tableView.delegate 33 | } 34 | 35 | public class func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) { 36 | let tableView: NSTableView = object as! NSTableView 37 | tableView.delegate = delegate as? NSTableViewDelegate 38 | } 39 | 40 | func tableViewSelectionDidChange(_ notification: Notification) { 41 | guard let numberOfRows = tableView?.numberOfRows, numberOfRows > 0 else { return } 42 | guard let selectedRow = tableView?.selectedRow else { return } 43 | 44 | let row: Int? = (selectedRow >= 0 && selectedRow < numberOfRows) ? selectedRow : nil 45 | 46 | selectedRowSubject.on(.next(row)) 47 | } 48 | 49 | } 50 | 51 | extension NSTableView { 52 | 53 | func createRxDelegateProxy() -> RxTableViewDelegateProxy { 54 | return RxTableViewDelegateProxy(parentObject: self) 55 | } 56 | 57 | } 58 | 59 | extension Reactive where Base: NSTableView { 60 | 61 | public var delegate: DelegateProxy { 62 | return RxTableViewDelegateProxy.proxyForObject(base) 63 | } 64 | 65 | public var selectedRow: ControlProperty { 66 | let delegate = RxTableViewDelegateProxy.proxyForObject(base) 67 | 68 | let source = Observable.deferred { [weak tableView = self.base] () -> Observable in 69 | if let startingRow = tableView?.selectedRow, startingRow >= 0 { 70 | return delegate.selectedRowSubject.startWith(startingRow) 71 | } else { 72 | return delegate.selectedRowSubject.startWith(nil) 73 | } 74 | }.takeUntil(deallocated) 75 | 76 | let observer = UIBindingObserver(UIElement: base) { (control, value: Int?) in 77 | if let row = value { 78 | control.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) 79 | } else { 80 | control.deselectAll(nil) 81 | } 82 | } 83 | 84 | return ControlProperty(values: source, valueSink: observer.asObserver()) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /NoteTaker/NSToolbarFlexibleSpaceItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSToolbarFlexibleSpaceItem.h 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 18/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | @import Cocoa; 10 | 11 | @interface NSToolbarFlexibleSpaceItem: NSToolbarItem 12 | 13 | @property (weak, nullable) NSSplitView *trackedSplitView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /NoteTaker/NeedsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeedsStorage.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import NoteTakerCore 11 | 12 | protocol NeedsStorage: class { 13 | 14 | var storage: NotesStorage? { get set } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /NoteTaker/NoteEditorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteEditorViewController.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import RxSwift 11 | import RxCocoa 12 | import NoteTakerCore 13 | 14 | class NoteEditorViewController: NSViewController, NeedsStorage { 15 | 16 | var note = Variable(.empty) 17 | var contents = Variable("") 18 | 19 | var storage: NotesStorage? 20 | 21 | private let disposeBag = DisposeBag() 22 | 23 | private lazy var editor: NoteEditorView = { 24 | let e = NoteEditorView(frame: self.view.bounds) 25 | 26 | e.autoresizingMask = [.viewWidthSizable, .viewHeightSizable] 27 | e.delegate = self 28 | 29 | return e 30 | }() 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | view.addSubview(editor) 36 | 37 | // track selected note 38 | note.asObservable() 39 | .observeOn(MainScheduler.instance) 40 | .subscribe(onNext: { [weak self] note in 41 | self?.updateNoteContents(with: note.body) 42 | }).addDisposableTo(disposeBag) 43 | 44 | // track note editing 45 | contents.asObservable() 46 | .observeOn(MainScheduler.instance) 47 | .distinctUntilChanged() 48 | .debounce(1, scheduler: MainScheduler.instance) 49 | .subscribe(onNext: { [weak self] contents in 50 | self?.didEditNote(with: contents) 51 | }).addDisposableTo(disposeBag) 52 | } 53 | 54 | private func updateNoteContents(with body: String) { 55 | editor.setContents(body) 56 | 57 | view.window?.makeFirstResponder(editor) 58 | } 59 | 60 | fileprivate func didEditNote(with body: String) { 61 | var note = self.note.value 62 | 63 | guard note.body != body else { return } 64 | 65 | note.modifiedAt = Date() 66 | note.body = body 67 | 68 | do { 69 | try storage?.store(note: note) 70 | } catch { 71 | NSLog("Error storing note: \(error)") 72 | } 73 | } 74 | 75 | } 76 | 77 | extension NoteEditorViewController: NoteEditorViewDelegate { 78 | 79 | func noteEditorView(_ sender: NoteEditorView, contentsDidChange contents: String) { 80 | self.contents.value = contents 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /NoteTaker/NoteTaker-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "NSTableView+Diff.h" 6 | #import "NSToolbarFlexibleSpaceItem.h" 7 | -------------------------------------------------------------------------------- /NoteTaker/NoteTaker.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | iCloud.br.com.guilhermerambo.NoteTakerShared 10 | 11 | com.apple.developer.icloud-services 12 | 13 | CloudKit 14 | 15 | com.apple.developer.ubiquity-kvstore-identifier 16 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 17 | 18 | 19 | -------------------------------------------------------------------------------- /NoteTaker/NotesSplitViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesSplitViewController.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import RxSwift 11 | import RxOptional 12 | import RxCocoa 13 | import CloudKit 14 | import NoteTakerCore 15 | 16 | /** 17 | NOTE: The code in this controller should probably live in a coordinator or something 18 | I usually avoid storyboards but used them here because this is a prototype, sample app 19 | */ 20 | 21 | class NotesSplitViewController: NSSplitViewController { 22 | 23 | private let disposeBag = DisposeBag() 24 | 25 | lazy var storage: NotesStorage = { 26 | return NotesStorage() 27 | }() 28 | 29 | var syncEngine: NotesSyncEngine! 30 | 31 | var notesController: NotesTableViewController { 32 | return childViewControllers[0] as! NotesTableViewController 33 | } 34 | 35 | var editorController: NoteEditorViewController { 36 | return childViewControllers[1] as! NoteEditorViewController 37 | } 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | childViewControllers.flatMap({ $0 as? NeedsStorage }).forEach({ $0.storage = storage }) 43 | 44 | syncEngine = NotesSyncEngine(storage: storage) 45 | 46 | view.wantsLayer = true 47 | 48 | notesController.selectedNote 49 | .asObservable() 50 | .replaceNilWith(.empty) 51 | .bindTo(editorController.note) 52 | .addDisposableTo(disposeBag) 53 | } 54 | 55 | override func viewDidAppear() { 56 | super.viewDidAppear() 57 | 58 | setupWindowStuff() 59 | } 60 | 61 | // Ideally, this window and toolbar setup code would live in a window controller, but this is just a prototype ;) 62 | private func setupWindowStuff() { 63 | guard let window = view.window else { return } 64 | 65 | window.titleVisibility = .hidden 66 | 67 | guard let toolbar = window.toolbar else { return } 68 | 69 | let flexibleItems = toolbar.items.flatMap({ $0 as? NSToolbarFlexibleSpaceItem }) 70 | 71 | guard flexibleItems.count > 1 else { return } 72 | 73 | flexibleItems[1].trackedSplitView = self.splitView 74 | } 75 | 76 | @IBAction func newNote(_ sender: Any?) { 77 | let note = Note(body: "New Note") 78 | 79 | do { 80 | try storage.store(note: note) 81 | } catch { 82 | NSAlert(error: error).runModal() 83 | } 84 | 85 | // make sure selectLatestNote is executed after the current runloop cycle so the recently created note is already there 86 | notesController.perform(#selector(NotesTableViewController.selectLatestNote), with: nil, afterDelay: 0) 87 | } 88 | 89 | @IBAction func shareNote(_ sender: Any?) { 90 | guard let window = view.window else { return } 91 | 92 | let alert = NSAlert() 93 | alert.messageText = "Sharing Not Available" 94 | alert.informativeText = "Sharing is not implemented yet" 95 | alert.addButton(withTitle: "OK") 96 | alert.beginSheetModal(for: window, completionHandler: nil) 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /NoteTaker/NotesTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesTableViewController.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import RxSwift 11 | import RxCocoa 12 | import NoteTakerCore 13 | 14 | class NotesTableViewController: NSViewController, NeedsStorage { 15 | 16 | @IBOutlet weak var tableView: NSTableView! 17 | 18 | var storage: NotesStorage? { 19 | didSet { 20 | guard let storage = storage else { return } 21 | 22 | subscribe(to: storage) 23 | } 24 | } 25 | 26 | var notes = Variable<[Note]>([]) 27 | 28 | var viewModels = [NoteViewModel]() { 29 | didSet { 30 | tableView.reloadData(withOldValue: oldValue, newValue: viewModels) 31 | 32 | // trigger table selection to make sure the selected note updates in the editor if needed 33 | perform(#selector(updateSelectedNote), with: nil, afterDelay: 0) 34 | } 35 | } 36 | 37 | lazy var selectedNote: Observable = { 38 | return self.tableView.rx.selectedRow.map { selectedRow -> Note? in 39 | if let row = selectedRow { 40 | return self.viewModels[row].note 41 | } else { 42 | return nil 43 | } 44 | } 45 | }() 46 | 47 | private let disposeBag = DisposeBag() 48 | 49 | override func viewDidLoad() { 50 | super.viewDidLoad() 51 | 52 | tableView.dataSource = self 53 | tableView.delegate = self 54 | 55 | notes.asObservable() 56 | .map({ $0.map(NoteViewModel.init) }) 57 | .subscribe { [weak self] event in 58 | switch event { 59 | case .next(let noteViewModels): 60 | self?.viewModels = noteViewModels 61 | break 62 | default: break 63 | } 64 | }.addDisposableTo(disposeBag) 65 | } 66 | 67 | private func subscribe(to storage: NotesStorage) { 68 | storage.allNotes.asObservable() 69 | .observeOn(MainScheduler.instance) 70 | .bindTo(notes) 71 | .addDisposableTo(disposeBag) 72 | } 73 | 74 | @objc func selectLatestNote() { 75 | tableView.rx.selectedRow.onNext(0) 76 | } 77 | 78 | @objc func updateSelectedNote() { 79 | // "flick" selection to force-refresh the note being edited 80 | let selection = self.tableView.selectedRow 81 | tableView.rx.selectedRow.onNext(nil) 82 | tableView.rx.selectedRow.onNext(selection) 83 | } 84 | 85 | @IBAction func delete(_ sender: Any) { 86 | let identifiers = tableView.selectedRowIndexes.map({ viewModels[$0].note.identifier }) 87 | 88 | do { 89 | try identifiers.forEach({ try storage?.delete(with: $0) }) 90 | } catch { 91 | let alert = NSAlert(error: error) 92 | 93 | if let window = view.window { 94 | alert.beginSheetModal(for: window, completionHandler: nil) 95 | } else { 96 | alert.runModal() 97 | } 98 | } 99 | } 100 | 101 | } 102 | 103 | extension NotesTableViewController: NSTableViewDataSource, NSTableViewDelegate { 104 | 105 | func numberOfRows(in tableView: NSTableView) -> Int { 106 | return viewModels.count 107 | } 108 | 109 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 110 | guard row < viewModels.count else { return nil } 111 | 112 | let cell: NSTableCellView? = tableView.make(withIdentifier: "cell", owner: tableView) as? NSTableCellView 113 | 114 | cell?.textField?.stringValue = viewModels[row].title 115 | 116 | return cell 117 | } 118 | 119 | func tableView(_ tableView: NSTableView, rowActionsForRow row: Int, edge: NSTableRowActionEdge) -> [NSTableViewRowAction] { 120 | let deleteAction = NSTableViewRowAction(style: .destructive, title: "Delete") { _, row in 121 | guard row < self.viewModels.count else { return } 122 | let note = self.viewModels[row].note 123 | 124 | do { 125 | try self.storage?.delete(with: note.identifier) 126 | } catch { 127 | NSAlert(error: error).runModal() 128 | } 129 | } 130 | 131 | return [deleteAction] 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /NoteTakerCore/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2017 Guilherme Rambo. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /NoteTakerCore/NoteTakerCore.h: -------------------------------------------------------------------------------- 1 | // 2 | // NoteTakerCore.h 3 | // NoteTakerCore 4 | // 5 | // Created by Guilherme Rambo on 27/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #if TARGET_OS_SIMULATOR || TARGET_OS_IOS 12 | #import 13 | #else 14 | #import 15 | #endif 16 | 17 | //! Project version number for NoteTakerCore. 18 | FOUNDATION_EXPORT double NoteTakerCoreVersionNumber; 19 | 20 | //! Project version string for NoteTakerCore. 21 | FOUNDATION_EXPORT const unsigned char NoteTakerCoreVersionString[]; 22 | 23 | // In this header, you should import all the public headers of your framework using statements like #import 24 | 25 | 26 | -------------------------------------------------------------------------------- /NoteTakerCore/Resources/Editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 | 34 | 35 |
{BODY}
36 | 37 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/Note+RealmNote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Note+RealmNote.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 12/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | extension Note { 13 | 14 | init(realmNote: RealmNote) { 15 | self.identifier = realmNote.identifier 16 | self.body = realmNote.body 17 | self.createdAt = realmNote.createdAt 18 | self.modifiedAt = realmNote.modifiedAt 19 | } 20 | 21 | var realmNote: RealmNote { 22 | let note = RealmNote() 23 | 24 | note.identifier = identifier 25 | note.body = body 26 | note.createdAt = createdAt 27 | note.modifiedAt = modifiedAt 28 | 29 | return note 30 | } 31 | 32 | } 33 | 34 | extension RealmNote { 35 | 36 | var note: Note { 37 | return Note(body: body, 38 | identifier: identifier, 39 | createdAt: createdAt, 40 | modifiedAt: modifiedAt) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/Note.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Note.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Note { 12 | 13 | public let identifier: String 14 | public let createdAt: Date 15 | public var modifiedAt: Date 16 | public var body: String 17 | 18 | public init(body: String, 19 | identifier: String? = nil, 20 | createdAt: Date? = nil, 21 | modifiedAt: Date? = nil) { 22 | self.body = body 23 | self.identifier = identifier ?? UUID().uuidString 24 | self.createdAt = createdAt ?? Date() 25 | self.modifiedAt = modifiedAt ?? Date() 26 | } 27 | 28 | } 29 | 30 | extension Note { 31 | 32 | public static var empty: Note { 33 | return Note(body: "") 34 | } 35 | 36 | } 37 | 38 | extension Note: Equatable { 39 | 40 | public static func ==(lhs: Note, rhs: Note) -> Bool { 41 | return lhs.identifier == rhs.identifier 42 | && lhs.createdAt == rhs.createdAt 43 | && lhs.modifiedAt == rhs.modifiedAt 44 | && lhs.body == rhs.body 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/NoteEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteEditorView.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 27/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import Cocoa 11 | 12 | public typealias UIView = NSView 13 | #else 14 | import UIKit 15 | #endif 16 | 17 | import WebKit 18 | 19 | public protocol NoteEditorViewDelegate: class { 20 | func noteEditorView(_ sender: NoteEditorView, contentsDidChange contents: String) 21 | } 22 | 23 | public final class NoteEditorView: UIView { 24 | 25 | public weak var delegate: NoteEditorViewDelegate? 26 | 27 | public fileprivate(set) var currentContents: String = "" 28 | 29 | public func setContents(_ contents: String) { 30 | loadEditor(with: contents) 31 | } 32 | 33 | fileprivate var webViewIsSettingContents = false 34 | 35 | #if os(macOS) 36 | 37 | public override var isFlipped: Bool { 38 | return true 39 | } 40 | 41 | public override init(frame frameRect: NSRect) { 42 | super.init(frame: frameRect) 43 | 44 | setup() 45 | } 46 | 47 | public required init?(coder: NSCoder) { 48 | super.init(coder: coder) 49 | 50 | setup() 51 | } 52 | 53 | public override func acceptsFirstMouse(for event: NSEvent?) -> Bool { 54 | return webView.acceptsFirstMouse(for: event) 55 | } 56 | 57 | public override var acceptsFirstResponder: Bool { 58 | return true 59 | } 60 | 61 | public override var canBecomeKeyView: Bool { 62 | return true 63 | } 64 | 65 | #else 66 | 67 | public override init(frame: CGRect) { 68 | super.init(frame: frame) 69 | 70 | setup() 71 | } 72 | 73 | public required init?(coder aDecoder: NSCoder) { 74 | super.init(coder: aDecoder) 75 | 76 | setup() 77 | } 78 | 79 | #endif 80 | 81 | public override func becomeFirstResponder() -> Bool { 82 | return webView.becomeFirstResponder() 83 | } 84 | 85 | public override func awakeFromNib() { 86 | super.awakeFromNib() 87 | 88 | setup() 89 | } 90 | 91 | private var setupDone = false 92 | 93 | private func setup() { 94 | guard !setupDone else { return } 95 | setupDone = true 96 | 97 | #if os(macOS) 98 | wantsLayer = true 99 | layer?.backgroundColor = NSColor.white.cgColor 100 | #else 101 | backgroundColor = .white 102 | #endif 103 | 104 | webView.frame = bounds 105 | addSubview(webView) 106 | } 107 | 108 | private lazy var webView: WKWebView = { 109 | let v = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) 110 | 111 | #if os(macOS) 112 | v.autoresizingMask = [.viewHeightSizable, .viewWidthSizable] 113 | #else 114 | v.autoresizingMask = [.flexibleWidth, .flexibleHeight] 115 | #endif 116 | 117 | v.configuration.userContentController.add(self, name: "editor") 118 | 119 | return v 120 | }() 121 | 122 | private lazy var editorSource: String? = { 123 | guard let editorSourceFile = Bundle(for: NoteEditorView.self).url(forResource: "Editor", withExtension: "html") else { return nil } 124 | guard let editorSourceData = try? Data(contentsOf: editorSourceFile) else { return nil } 125 | 126 | return String(data: editorSourceData, encoding: .utf8) 127 | }() 128 | 129 | private func loadEditor(with contents: String = "") { 130 | guard let editorSource = editorSource else { return } 131 | 132 | webView.loadHTMLString(editorSource.replacingOccurrences(of: "{BODY}", with: contents), baseURL: nil) 133 | } 134 | 135 | } 136 | 137 | private enum WebViewMessages: String { 138 | case loaded 139 | case input 140 | } 141 | 142 | extension NoteEditorView: WKScriptMessageHandler { 143 | 144 | public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 145 | guard let inputStr = message.body as? String else { return } 146 | 147 | DispatchQueue.main.async { 148 | self.currentContents = inputStr 149 | 150 | self.delegate?.noteEditorView(self, contentsDidChange: inputStr) 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/NoteViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteViewModel.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import IGListKit 11 | 12 | public class NoteViewModel: NSObject, IGListDiffable { 13 | 14 | public let note: Note 15 | 16 | public lazy var title: String = { 17 | return self.note.body.firstLine.removingHTML 18 | }() 19 | 20 | public init(note: Note) { 21 | self.note = note 22 | 23 | super.init() 24 | } 25 | 26 | public func diffIdentifier() -> NSObjectProtocol { 27 | return note.identifier as NSObjectProtocol 28 | } 29 | 30 | public func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { 31 | guard let other = object as? NoteViewModel else { return false } 32 | 33 | return other.note == self.note 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/NotesStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesStorage.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 10/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import RxRealm 12 | import RxSwift 13 | 14 | enum StorageError: Error { 15 | case recordNotFound(String) 16 | 17 | var localizedDescription: String { 18 | switch self { 19 | case .recordNotFound(let identifier): 20 | return "Record not found with primary key \(identifier)" 21 | } 22 | } 23 | } 24 | 25 | /// This class is responsible for the management of the local database (fetching, saving and deleting notes) 26 | public final class NotesStorage { 27 | 28 | typealias UpdateDecisionHandler = (_ oldObject: T, _ newObject: T) -> Bool 29 | 30 | let realm: Realm 31 | 32 | init(realm: Realm? = nil) { 33 | if let r = realm { 34 | self.realm = r 35 | } else { 36 | self.realm = try! Realm() 37 | } 38 | } 39 | 40 | public convenience init() { 41 | self.init(realm: nil) 42 | } 43 | 44 | public var allNotes: Observable<[Note]> { 45 | // Instead of immediately deleting notes when they are deleted in the UI, 46 | // we simply set a flag on them that is used to delete the record on the Cloud 47 | // This is necessary because of the way Realm collection notifications work, 48 | // it is the best workaround I've found 49 | let objects = self.realm.objects(RealmNote.self) 50 | .filter(NSPredicate(format: "isDeleted == false")) 51 | .sorted(byKeyPath: "modifiedAt", ascending: false) 52 | 53 | return Observable.collection(from: objects).map { realmNotes in 54 | return realmNotes.map({ $0.note }) 55 | } 56 | } 57 | 58 | var mostRecentlyModifiedNote: Note? { 59 | let realmNotes = realm.objects(RealmNote.self) 60 | .sorted(byKeyPath: NoteKey.modifiedAt.rawValue, ascending: false) 61 | 62 | return realmNotes.first?.note 63 | } 64 | 65 | public func store(note: Note) throws { 66 | try store(realmNote: note.realmNote) 67 | } 68 | 69 | func store(realmNote: RealmNote, notNotifying token: NotificationToken? = nil) throws { 70 | try insertOrUpdate(object: realmNote, notNotifying: token) { oldNote, newNote in 71 | guard newNote != oldNote else { return false } 72 | 73 | return newNote.modifiedAt > oldNote.modifiedAt 74 | } 75 | } 76 | 77 | public func delete(with identifier: String, hard reallyDelete: Bool = false) throws { 78 | guard let note = realm.object(ofType: RealmNote.self, forPrimaryKey: identifier) else { 79 | throw StorageError.recordNotFound(identifier) 80 | } 81 | 82 | try realm.write { 83 | if reallyDelete { 84 | self.realm.delete(note) 85 | } else { 86 | note.isDeleted = true 87 | self.realm.add(note, update: true) 88 | } 89 | } 90 | } 91 | 92 | func deletePreviouslySoftDeletedNotes(notNotifying token: NotificationToken? = nil) throws { 93 | let objects = realm.objects(RealmNote.self).filter("isDeleted = true") 94 | 95 | let tokens: [NotificationToken] 96 | 97 | if let token = token { 98 | tokens = [token] 99 | } else { 100 | tokens = [] 101 | } 102 | 103 | realm.beginWrite() 104 | objects.forEach({ realm.delete($0) }) 105 | try realm.commitWrite(withoutNotifying: tokens) 106 | } 107 | 108 | private func insertOrUpdate(objects: [T], 109 | notNotifying token: NotificationToken? = nil, 110 | updateDecisionHandler: @escaping UpdateDecisionHandler) throws { 111 | try objects.forEach({ try self.insertOrUpdate(object: $0, notNotifying: token, updateDecisionHandler: updateDecisionHandler) }) 112 | } 113 | 114 | private func insertOrUpdate(object: T, 115 | notNotifying token: NotificationToken? = nil, 116 | updateDecisionHandler: @escaping UpdateDecisionHandler) throws { 117 | guard let primaryKey = T.primaryKey() else { 118 | fatalError("insertOrUpdate can't be used for objects without a primary key") 119 | } 120 | 121 | guard let primaryKeyValue = object.value(forKey: primaryKey) else { 122 | fatalError("insertOrUpdate can't be used for objects without a primary key") 123 | } 124 | 125 | let tokens: [NotificationToken] 126 | 127 | if let token = token { 128 | tokens = [token] 129 | } else { 130 | tokens = [] 131 | } 132 | 133 | if let existingObject = realm.object(ofType: T.self, forPrimaryKey: primaryKeyValue) { 134 | // object already exists, call updateDecisionHandler to determine whether we should update it or not 135 | if updateDecisionHandler(existingObject, object) { 136 | realm.beginWrite() 137 | realm.add(object, update: true) 138 | try realm.commitWrite(withoutNotifying: tokens) 139 | } 140 | } else { 141 | // object doesn't exist, just add it 142 | realm.beginWrite() 143 | realm.add(object) 144 | try realm.commitWrite(withoutNotifying: tokens) 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/NotesSyncEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesSyncEngine.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 11/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import RxRealm 12 | import RxSwift 13 | import CloudKit 14 | 15 | extension CKContainer { 16 | static let noteTakerShared = CKContainer(identifier: "iCloud.br.com.guilhermerambo.NoteTakerShared") 17 | } 18 | 19 | extension Notification.Name { 20 | public static let notesDidChangeRemotely = Notification.Name(rawValue: "NotesDidChangeRemotely") 21 | } 22 | 23 | private func slog(_ format: String, _ args: CVarArg...) { 24 | guard ProcessInfo.processInfo.arguments.contains("--log-sync") else { return } 25 | 26 | NSLog("[SYNC] " + format, args) 27 | } 28 | 29 | /// This class is responsible for observing changes to the local database and pushing them to CloudKit 30 | /// as well as observing changes in CloudKit and syncing them to the local database 31 | public final class NotesSyncEngine: NSObject { 32 | 33 | private struct Constants { 34 | static let previousChangeToken = "PreviousChangeToken" 35 | static let noteRecordType = "Note" 36 | } 37 | 38 | /// The CloudKit container the sync engine is using 39 | private let container: CKContainer 40 | 41 | /// The user's private CloudKit database 42 | private let privateDatabase: CKDatabase 43 | 44 | /// Local storage controller 45 | private let storage: NotesStorage 46 | 47 | /// Initializes the sync engine with a local storage 48 | public init(storage: NotesStorage, container: CKContainer = .noteTakerShared) { 49 | self.storage = storage 50 | self.container = container 51 | self.privateDatabase = container.privateCloudDatabase 52 | 53 | super.init() 54 | 55 | // Fetch notifications not processed yet 56 | fetchServerNotifications() 57 | 58 | // Do initial cloud fetch 59 | fetchCloudKitNotes() 60 | 61 | // Sync magic 62 | subscribeToLocalDatabaseChanges() 63 | subscribeToCloudKitChanges() 64 | 65 | // Clean database before the app terminates 66 | #if os(macOS) 67 | NotificationCenter.default.addObserver(self, selector: #selector(cleanup(_:)), name: .NSApplicationWillTerminate, object: NSApp) 68 | #else 69 | NotificationCenter.default.addObserver(self, selector: #selector(cleanup(_:)), name: .UIApplicationWillTerminate, object: UIApplication.shared) 70 | #endif 71 | } 72 | 73 | /// The modification date of the last note modified locally to use when querying the server 74 | private var modificationDateForQuery: Date { 75 | return storage.mostRecentlyModifiedNote?.modifiedAt ?? Date.distantPast 76 | } 77 | 78 | /// Download notes from CloudKit 79 | private func fetchCloudKitNotes(_ inputCursor: CKQueryCursor? = nil) { 80 | let operation: CKQueryOperation 81 | 82 | // We may be starting a new query or continuing a previous one if there are many results 83 | if let cursor = inputCursor { 84 | operation = CKQueryOperation(cursor: cursor) 85 | } else { 86 | // This query will fetch all notes modified since the last sync, sorted by modification date (descending) 87 | let predicate = NSPredicate(format: "modifiedAt > %@", modificationDateForQuery as CVarArg) 88 | let query = CKQuery(recordType: Constants.noteRecordType, predicate: predicate) 89 | query.sortDescriptors = [NSSortDescriptor(key: NoteKey.modifiedAt.rawValue, ascending: false)] 90 | operation = CKQueryOperation(query: query) 91 | } 92 | 93 | operation.queryCompletionBlock = { [weak self] cursor, error in 94 | guard error == nil else { 95 | self?.retryCloudKitOperationIfPossible(with: error) { 96 | self?.fetchCloudKitNotes(inputCursor) 97 | } 98 | 99 | return 100 | } 101 | 102 | if let cursor = cursor { 103 | // There are more results to come, continue fetching 104 | self?.fetchCloudKitNotes(cursor) 105 | } 106 | } 107 | 108 | operation.recordFetchedBlock = { [weak self] record in 109 | // When a note is fetched from the cloud, process it into the local database 110 | self?.processFetchedNote(record) 111 | } 112 | 113 | privateDatabase.add(operation) 114 | } 115 | 116 | private let disposeBag = DisposeBag() 117 | 118 | /// Realm collection notification token 119 | private var notificationToken: NotificationToken? 120 | 121 | private func subscribeToLocalDatabaseChanges() { 122 | let notes = storage.realm.objects(RealmNote.self) 123 | 124 | // Here we subscribe to changes in notes to push them to CloudKit 125 | notificationToken = notes.addNotificationBlock { [weak self] changes in 126 | guard let welf = self else { return } 127 | 128 | switch changes { 129 | case .update(let collection, _, let insertions, let modifications): 130 | // Figure out which notes should be saved and which notes should be deleted 131 | let notesToSave = (insertions + modifications).map({ collection[$0] }).filter({ !$0.isDeleted }) 132 | let notesToDelete = modifications.map({ collection[$0] }).filter({ $0.isDeleted }) 133 | 134 | // Push changes to CloudKitx 135 | welf.pushToCloudKit(notesToUpdate: notesToSave, notesToDelete: notesToDelete) 136 | case .error(let error): 137 | slog("Realm notification error: \(error)") 138 | default: break 139 | } 140 | } 141 | } 142 | 143 | fileprivate func pushToCloudKit(notesToUpdate: [RealmNote], notesToDelete: [RealmNote]) { 144 | guard notesToUpdate.count > 0 || notesToDelete.count > 0 else { return } 145 | 146 | slog("\(notesToUpdate.count) note(s) to save, \(notesToDelete.count) note(s) to delete") 147 | 148 | let recordsToSave = notesToUpdate.map({ $0.record }) 149 | let recordsToDelete = notesToDelete.map({ $0.recordID }) 150 | 151 | pushRecordsToCloudKit(recordsToUpdate: recordsToSave, recordIDsToDelete: recordsToDelete) 152 | } 153 | 154 | fileprivate func pushRecordsToCloudKit(recordsToUpdate: [CKRecord], recordIDsToDelete: [CKRecordID], completion: ((Error?) -> ())? = nil) { 155 | let operation = CKModifyRecordsOperation(recordsToSave: recordsToUpdate, recordIDsToDelete: recordIDsToDelete) 156 | operation.savePolicy = .changedKeys 157 | 158 | operation.modifyRecordsCompletionBlock = { [weak self] _, _, error in 159 | guard error == nil else { 160 | slog("Error modifying records: \(error!)") 161 | 162 | self?.retryCloudKitOperationIfPossible(with: error) { 163 | self?.pushRecordsToCloudKit(recordsToUpdate: recordsToUpdate, 164 | recordIDsToDelete: recordIDsToDelete, 165 | completion: completion) 166 | } 167 | return 168 | } 169 | 170 | slog("Finished saving records") 171 | 172 | DispatchQueue.main.async { 173 | completion?(nil) 174 | } 175 | } 176 | 177 | privateDatabase.add(operation) 178 | } 179 | 180 | private func subscribeToCloudKitChanges() { 181 | startObservingCloudKitChanges() 182 | 183 | // Create the CloudKit subscription so we receive push notifications when notes change remotely 184 | let subscription = CKQuerySubscription(recordType: Constants.noteRecordType, 185 | predicate: NSPredicate(value: true), 186 | options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]) 187 | 188 | let info = CKNotificationInfo() 189 | info.shouldSendContentAvailable = true 190 | info.soundName = "" 191 | subscription.notificationInfo = info 192 | 193 | privateDatabase.save(subscription) { [weak self] subscription, error in 194 | if subscription != nil { 195 | slog("Successfully subscribed to cloud database changes") 196 | } else { 197 | guard error == nil else { 198 | self?.retryCloudKitOperationIfPossible(with: error) { 199 | self?.subscribeToCloudKitChanges() 200 | } 201 | return 202 | } 203 | } 204 | } 205 | } 206 | 207 | /// Holds the latest change token we got from CloudKit, storing it in UserDefaults 208 | private var previousChangeToken: CKServerChangeToken? { 209 | get { 210 | guard let tokenData = UserDefaults.standard.object(forKey: Constants.previousChangeToken) as? Data else { return nil } 211 | 212 | return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? CKServerChangeToken 213 | } 214 | set { 215 | guard let newValue = newValue else { 216 | UserDefaults.standard.setNilValueForKey(Constants.previousChangeToken) 217 | return 218 | } 219 | 220 | let data = NSKeyedArchiver.archivedData(withRootObject: newValue) 221 | 222 | UserDefaults.standard.set(data, forKey: Constants.previousChangeToken) 223 | } 224 | } 225 | 226 | // CloudKit notes observer 227 | private var changesObserver: NSObjectProtocol? 228 | 229 | private func startObservingCloudKitChanges() { 230 | // The .notesDidChangeRemotely local notification is posted by the AppDelegate when it receives a push notification from CloudKit 231 | changesObserver = NotificationCenter.default.addObserver(forName: .notesDidChangeRemotely, 232 | object: nil, 233 | queue: OperationQueue.main) 234 | { [weak self] note in 235 | // When a notification is received from the server, we must download the notifications because they might have been coalesced 236 | self?.fetchServerNotifications() 237 | } 238 | } 239 | 240 | private func fetchServerNotifications() { 241 | let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: previousChangeToken) 242 | 243 | // This will hold the identifiers for every changed record 244 | var updatedIdentifiers = [CKRecordID]() 245 | 246 | // This will hold the notification IDs we processed so we can tell CloudKit to never send them to us again 247 | var notificationIDs = [CKNotificationID]() 248 | 249 | operation.notificationChangedBlock = { [weak self] notification in 250 | guard let notification = notification as? CKQueryNotification else { return } 251 | guard let identifier = notification.recordID else { return } 252 | 253 | if let id = notification.notificationID { 254 | notificationIDs.append(id) 255 | } 256 | 257 | DispatchQueue.main.async { 258 | switch notification.queryNotificationReason { 259 | case .recordDeleted: 260 | do { 261 | try self?.storage.delete(with: identifier.recordName, hard: true) 262 | } catch { 263 | slog("Error deleting note from cloud instruction: \(error)") 264 | } 265 | default: 266 | updatedIdentifiers.append(identifier) 267 | } 268 | } 269 | } 270 | 271 | operation.fetchNotificationChangesCompletionBlock = { [weak self] newToken, error in 272 | guard error == nil else { 273 | self?.retryCloudKitOperationIfPossible(with: error) { 274 | self?.fetchServerNotifications() 275 | } 276 | 277 | return 278 | } 279 | 280 | self?.previousChangeToken = newToken 281 | 282 | // All records are in, now save the data locally 283 | self?.consolidateUpdatedCloudNotes(with: updatedIdentifiers) 284 | 285 | // Tell CloudKit we've read the notifications 286 | self?.markNotificationsAsRead(with: notificationIDs) 287 | } 288 | 289 | container.add(operation) 290 | } 291 | 292 | private func markNotificationsAsRead(with identifiers: [CKNotificationID]) { 293 | let operation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: identifiers) 294 | 295 | operation.markNotificationsReadCompletionBlock = { [weak self] _, error in 296 | guard error == nil else { 297 | self?.retryCloudKitOperationIfPossible(with: error) { 298 | self?.markNotificationsAsRead(with: identifiers) 299 | } 300 | 301 | return 302 | } 303 | } 304 | 305 | container.add(operation) 306 | } 307 | 308 | /// Download a list of records from CloudKit and update the local database accordingly 309 | private func consolidateUpdatedCloudNotes(with identifiers: [CKRecordID]) { 310 | let operation = CKFetchRecordsOperation(recordIDs: identifiers) 311 | 312 | operation.fetchRecordsCompletionBlock = { [weak self] records, error in 313 | guard let records = records else { 314 | self?.retryCloudKitOperationIfPossible(with: error) { 315 | self?.consolidateUpdatedCloudNotes(with: identifiers) 316 | } 317 | return 318 | } 319 | 320 | records.values.forEach { record in 321 | self?.processFetchedNote(record) 322 | } 323 | } 324 | 325 | privateDatabase.add(operation) 326 | } 327 | 328 | /// Sync a single note to the local database 329 | private func processFetchedNote(_ cloudKitNote: CKRecord) { 330 | DispatchQueue.main.async { 331 | guard let note = RealmNote.from(record: cloudKitNote) else { 332 | slog("Error creating local note from cloud note \(cloudKitNote.recordID.recordName)") 333 | return 334 | } 335 | 336 | do { 337 | try self.storage.store(realmNote: note, notNotifying: self.notificationToken) 338 | } catch { 339 | slog("Error saving local note from cloud note \(cloudKitNote.recordID.recordName): \(error)") 340 | } 341 | } 342 | } 343 | 344 | @objc func cleanup(_ notification: Notification? = nil) { 345 | do { 346 | try storage.deletePreviouslySoftDeletedNotes(notNotifying: self.notificationToken) 347 | } catch { 348 | NSLog("Failed to delete previously soft deleted notes: \(error)") 349 | } 350 | } 351 | 352 | // MARK: - Util 353 | 354 | /// Helper method to retry a CloudKit operation when its error suggests it 355 | private func retryCloudKitOperationIfPossible(with error: Error?, block: @escaping () -> ()) { 356 | guard let error = error as? CKError else { 357 | slog("CloudKit puked ¯\\_(ツ)_/¯") 358 | return 359 | } 360 | 361 | guard let retryAfter = error.userInfo[CKErrorRetryAfterKey] as? NSNumber else { 362 | slog("CloudKit error: \(error)") 363 | return 364 | } 365 | 366 | slog("CloudKit operation error, retrying after \(retryAfter) seconds...") 367 | 368 | DispatchQueue.main.asyncAfter(deadline: .now() + retryAfter.doubleValue) { 369 | block() 370 | } 371 | } 372 | 373 | } 374 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/RealmNote+CKRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmNote+CKRecord.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 12/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CloudKit 11 | import RealmSwift 12 | 13 | enum NoteKey: String { 14 | case identifier, createdAt, modifiedAt, body 15 | } 16 | 17 | extension CKRecord { 18 | 19 | subscript(_ key: NoteKey) -> CKRecordValue { 20 | get { 21 | return self[key.rawValue]! 22 | } 23 | set { 24 | self[key.rawValue] = newValue 25 | } 26 | } 27 | 28 | } 29 | 30 | extension RealmNote { 31 | 32 | var recordID: CKRecordID { 33 | return CKRecordID(recordName: identifier) 34 | } 35 | 36 | var record: CKRecord { 37 | let record = CKRecord(recordType: "Note", recordID: recordID) 38 | 39 | record[.identifier] = identifier as CKRecordValue 40 | record[.createdAt] = createdAt as CKRecordValue 41 | record[.modifiedAt] = modifiedAt as CKRecordValue 42 | record[.body] = body as CKRecordValue 43 | 44 | return record 45 | } 46 | 47 | static func from(record: CKRecord) -> RealmNote? { 48 | guard let identifier = record[.identifier] as? String, 49 | let createdAt = record[.createdAt] as? Date, 50 | let modifiedAt = record[.modifiedAt] as? Date, 51 | let body = record[.body] as? String 52 | else { 53 | return nil 54 | } 55 | 56 | let note = RealmNote() 57 | 58 | note.identifier = identifier 59 | note.createdAt = createdAt 60 | note.modifiedAt = modifiedAt 61 | note.body = body 62 | 63 | return note 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/RealmNote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmNote.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 12/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | final class RealmNote: Object { 13 | 14 | dynamic var identifier = "" 15 | dynamic var createdAt = Date() 16 | dynamic var modifiedAt = Date() 17 | dynamic var body = "" 18 | dynamic var isDeleted = false 19 | 20 | override class func primaryKey() -> String? { 21 | return "identifier" 22 | } 23 | 24 | static func ==(lhs: RealmNote, rhs: RealmNote) -> Bool { 25 | return lhs.note == rhs.note && lhs.isDeleted == rhs.isDeleted 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /NoteTakerCore/Source/String+HTML.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+HTML.swift 3 | // NoteTaker 4 | // 5 | // Created by Guilherme Rambo on 12/02/17. 6 | // Copyright © 2017 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | var parsingHTML: NSAttributedString { 14 | let attrs: [String: Any] = [ 15 | NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, 16 | NSCharacterEncodingDocumentAttribute: String.Encoding.utf8 17 | ] 18 | 19 | return NSAttributedString(string: self, attributes: attrs) 20 | } 21 | 22 | var removingHTML: String { 23 | return replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil) 24 | .replacingOccurrences(of: "&[^\\s]*;", with: "", options: .regularExpression, range: nil) 25 | } 26 | 27 | var firstLine: String { 28 | return components(separatedBy: .newlines).first ?? self 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NoteTaker 2 | 3 | A simple note taking app for macOS and iOS which uses [Realm](https://realm.io) and [CloudKit](https://developer.apple.com/icloud) for syncing. 4 | 5 | This is a sample project to demonstate an architecture for syncing described in my article on CloudKit. 6 | 7 | *Notice: the iOS version of the app only lists the notes and is only available to demonstrate syncing between devices/platforms* 8 | 9 | ![screenshot](./screenshot.png) 10 | 11 | ## Building 12 | 13 | Building requires: 14 | 15 | - Xcode 8.2.1 16 | - Carthage 17 | - Apple Developer account 18 | 19 | Before opening the project in Xcode, you must build its dependencies with Carthage (this can take several minutes, but has to be done only once): 20 | 21 | ```bash 22 | carthage update 23 | ``` -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/NoteTaker/db7e5f1fe60c18b4f27209d7a0889b1f199a383b/screenshot.png --------------------------------------------------------------------------------