diff --git a/Shared/Extensions/WriteFreelyModel+API.swift b/Shared/Extensions/WriteFreelyModel+API.swift new file mode 100644 index 0000000..b13bd4b --- /dev/null +++ b/Shared/Extensions/WriteFreelyModel+API.swift @@ -0,0 +1,152 @@ +import Foundation +import WriteFreely + +extension WriteFreelyModel { + func login(to server: URL, as username: String, password: String) { + if !hasNetworkConnection { + isPresentingNetworkErrorAlert = true + return + } + let secureProtocolPrefix = "https://" + let insecureProtocolPrefix = "http://" + var serverString = server.absoluteString + // If there's neither an http or https prefix, prepend "https://" to the server string. + if !(serverString.hasPrefix(secureProtocolPrefix) || serverString.hasPrefix(insecureProtocolPrefix)) { + serverString = secureProtocolPrefix + serverString + } + // If the server string is prefixed with http, upgrade to https before attempting to login. + if serverString.hasPrefix(insecureProtocolPrefix) { + serverString = serverString.replacingOccurrences(of: insecureProtocolPrefix, with: secureProtocolPrefix) + } + isLoggingIn = true + var serverURL = URL(string: serverString)! + if !serverURL.path.isEmpty { + serverURL.deleteLastPathComponent() + } + account.server = serverURL.absoluteString + client = WFClient(for: serverURL) + client?.login(username: username, password: password, completion: loginHandler) + } + + func logout() { + if !hasNetworkConnection { + DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + return + } + guard let loggedInClient = client else { + do { + try purgeTokenFromKeychain(username: account.username, server: account.server) + account.logout() + } catch { + fatalError("Failed to log out persisted state") + } + return + } + loggedInClient.logout(completion: logoutHandler) + } + + func fetchUserCollections() { + if !hasNetworkConnection { + DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + return + } + guard let loggedInClient = client else { return } + // We're starting the network request. + DispatchQueue.main.async { + self.isProcessingRequest = true + } + loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler) + } + + func fetchUserPosts() { + if !hasNetworkConnection { + DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + return + } + guard let loggedInClient = client else { return } + // We're starting the network request. + DispatchQueue.main.async { + self.isProcessingRequest = true + } + loggedInClient.getPosts(completion: fetchUserPostsHandler) + } + + func publish(post: WFAPost) { + postToUpdate = nil + + if !hasNetworkConnection { + DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + return + } + guard let loggedInClient = client else { return } + // We're starting the network request. + DispatchQueue.main.async { + self.isProcessingRequest = true + } + + if post.language == nil { + if let languageCode = Locale.current.languageCode { + post.language = languageCode + post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft + } + } + + var wfPost = WFPost( + body: post.body, + title: post.title.isEmpty ? "" : post.title, + appearance: post.appearance, + language: post.language, + rtl: post.rtl, + createdDate: post.createdDate + ) + + if let existingPostId = post.postId { + // This is an existing post. + postToUpdate = post + wfPost.postId = post.postId + + loggedInClient.updatePost( + postId: existingPostId, + updatedPost: wfPost, + completion: publishHandler + ) + } else { + // This is a new local draft. + loggedInClient.createPost( + post: wfPost, in: post.collectionAlias, completion: publishHandler + ) + } + } + + func updateFromServer(post: WFAPost) { + if !hasNetworkConnection { + DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + return + } + guard let loggedInClient = client else { return } + guard let postId = post.postId else { return } + // We're starting the network request. + DispatchQueue.main.async { + self.selectedPost = post + self.isProcessingRequest = true + } + loggedInClient.getPost(byId: postId, completion: updateFromServerHandler) + } + + func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) { + if !hasNetworkConnection { + DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } + return + } + guard let loggedInClient = client, + let postId = post.postId else { return } + // We're starting the network request. + DispatchQueue.main.async { + self.isProcessingRequest = true + } + + selectedPost = post + post.collectionAlias = newCollection?.alias + loggedInClient.movePost(postId: postId, to: newCollection?.alias, completion: movePostHandler) + } +} diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift similarity index 55% copy from Shared/Models/WriteFreelyModel.swift copy to Shared/Extensions/WriteFreelyModel+APIHandlers.swift index 286f39b..9000ace 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift @@ -1,571 +1,294 @@ import Foundation import WriteFreely -import Security -import Network - -// MARK: - WriteFreelyModel - -class WriteFreelyModel: ObservableObject { - @Published var account = AccountModel() - @Published var preferences = PreferencesModel() - @Published var posts = PostListModel() - @Published var editor = PostEditorModel() - @Published var isLoggingIn: Bool = false - @Published var isProcessingRequest: Bool = false - @Published var hasNetworkConnection: Bool = true - @Published var selectedPost: WFAPost? - @Published var selectedCollection: WFACollection? - @Published var showAllPosts: Bool = true - @Published var isPresentingDeleteAlert: Bool = false - @Published var isPresentingLoginErrorAlert: Bool = false - @Published var isPresentingNetworkErrorAlert: Bool = false - @Published var postToDelete: WFAPost? - #if os(iOS) - @Published var isPresentingSettingsView: Bool = false - #endif - - var loginErrorMessage: String? - - // swiftlint:disable line_length - let helpURL = URL(string: "https://discuss.write.as/c/help/5")! - let howToURL = URL(string: "https://discuss.write.as/t/using-the-writefreely-ios-app/1946")! - let reviewURL = URL(string: "https://apps.apple.com/app/id1531530896?action=write-review")! - let licensesURL = URL(string: "https://github.com/writeas/writefreely-swiftui-multiplatform/tree/main/Shared/Resources/Licenses")! - // swiftlint:enable line_length - - private var client: WFClient? - private let defaults = UserDefaults.standard - private let monitor = NWPathMonitor() - private let queue = DispatchQueue(label: "NetworkMonitor") - private var postToUpdate: WFAPost? - - init() { - DispatchQueue.main.async { - self.preferences.appearance = self.defaults.integer(forKey: self.preferences.colorSchemeIntegerKey) - self.preferences.font = self.defaults.integer(forKey: self.preferences.defaultFontIntegerKey) - self.account.restoreState() - if self.account.isLoggedIn { - guard let serverURL = URL(string: self.account.server) else { - print("Server URL not found") - return - } - guard let token = self.fetchTokenFromKeychain( - username: self.account.username, - server: self.account.server - ) else { - print("Could not fetch token from Keychain") - return - } - self.account.login(WFUser(token: token, username: self.account.username)) - self.client = WFClient(for: serverURL) - self.client?.user = self.account.user - self.fetchUserCollections() - self.fetchUserPosts() - } - } - - monitor.pathUpdateHandler = { path in - DispatchQueue.main.async { - self.hasNetworkConnection = path.status == .satisfied - } - } - monitor.start(queue: queue) - } -} - -// MARK: - WriteFreelyModel API extension WriteFreelyModel { - func login(to server: URL, as username: String, password: String) { - if !hasNetworkConnection { - isPresentingNetworkErrorAlert = true - return - } - let secureProtocolPrefix = "https://" - let insecureProtocolPrefix = "http://" - var serverString = server.absoluteString - // If there's neither an http or https prefix, prepend "https://" to the server string. - if !(serverString.hasPrefix(secureProtocolPrefix) || serverString.hasPrefix(insecureProtocolPrefix)) { - serverString = secureProtocolPrefix + serverString - } - // If the server string is prefixed with http, upgrade to https before attempting to login. - if serverString.hasPrefix(insecureProtocolPrefix) { - serverString = serverString.replacingOccurrences(of: insecureProtocolPrefix, with: secureProtocolPrefix) - } - isLoggingIn = true - var serverURL = URL(string: serverString)! - if !serverURL.path.isEmpty { - serverURL.deleteLastPathComponent() - } - account.server = serverURL.absoluteString - client = WFClient(for: serverURL) - client?.login(username: username, password: password, completion: loginHandler) - } - - func logout() { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { - do { - try purgeTokenFromKeychain(username: account.username, server: account.server) - account.logout() - } catch { - fatalError("Failed to log out persisted state") - } - return - } - loggedInClient.logout(completion: logoutHandler) - } - - func fetchUserCollections() { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler) - } - - func fetchUserPosts() { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - loggedInClient.getPosts(completion: fetchUserPostsHandler) - } - - func publish(post: WFAPost) { - postToUpdate = nil - - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - - if post.language == nil { - if let languageCode = Locale.current.languageCode { - post.language = languageCode - post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft - } - } - - var wfPost = WFPost( - body: post.body, - title: post.title.isEmpty ? "" : post.title, - appearance: post.appearance, - language: post.language, - rtl: post.rtl, - createdDate: post.createdDate - ) - - if let existingPostId = post.postId { - // This is an existing post. - postToUpdate = post - wfPost.postId = post.postId - - loggedInClient.updatePost( - postId: existingPostId, - updatedPost: wfPost, - completion: publishHandler - ) - } else { - // This is a new local draft. - loggedInClient.createPost( - post: wfPost, in: post.collectionAlias, completion: publishHandler - ) - } - } - - func updateFromServer(post: WFAPost) { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - guard let postId = post.postId else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.selectedPost = post - self.isProcessingRequest = true - } - loggedInClient.getPost(byId: postId, completion: updateFromServerHandler) - } - - func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client, - let postId = post.postId else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - - selectedPost = post - post.collectionAlias = newCollection?.alias - loggedInClient.movePost(postId: postId, to: newCollection?.alias, completion: movePostHandler) - } -} - -private extension WriteFreelyModel { func loginHandler(result: Result) { DispatchQueue.main.async { self.isLoggingIn = false } do { let user = try result.get() fetchUserCollections() fetchUserPosts() saveTokenToKeychain(user.token, username: user.username, server: account.server) DispatchQueue.main.async { self.account.login(user) } } catch WFError.notFound { DispatchQueue.main.async { self.loginErrorMessage = AccountError.usernameNotFound.localizedDescription self.isPresentingLoginErrorAlert = true } } catch WFError.unauthorized { DispatchQueue.main.async { self.loginErrorMessage = AccountError.invalidPassword.localizedDescription self.isPresentingLoginErrorAlert = true } } catch { if (error as NSError).domain == NSURLErrorDomain, (error as NSError).code == -1003 { DispatchQueue.main.async { self.loginErrorMessage = AccountError.serverNotFound.localizedDescription self.isPresentingLoginErrorAlert = true } } else { DispatchQueue.main.async { self.loginErrorMessage = error.localizedDescription self.isPresentingLoginErrorAlert = true } } } } func logoutHandler(result: Result) { do { _ = try result.get() do { try purgeTokenFromKeychain(username: account.user?.username, server: account.server) client = nil DispatchQueue.main.async { self.account.logout() LocalStorageManager().purgeUserCollections() self.posts.purgePublishedPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } catch WFError.notFound { // The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with // purging the token from the Keychain, destroying the client object, and setting the AccountModel to its // logged-out state. do { try purgeTokenFromKeychain(username: account.user?.username, server: account.server) client = nil DispatchQueue.main.async { self.account.logout() LocalStorageManager().purgeUserCollections() self.posts.purgePublishedPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } catch { // We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here, // so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're // logged in, try calling the logout function again and see what we get. // Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties. if (error as NSError).domain == NSURLErrorDomain, (error as NSError).code == NSURLErrorCannotParseResponse { if account.isLoggedIn { self.logout() } } } } func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) { // We're done with the network request. DispatchQueue.main.async { self.isProcessingRequest = false } do { let fetchedCollections = try result.get() for fetchedCollection in fetchedCollections { DispatchQueue.main.async { let localCollection = WFACollection(context: LocalStorageManager.persistentContainer.viewContext) localCollection.alias = fetchedCollection.alias localCollection.blogDescription = fetchedCollection.description localCollection.email = fetchedCollection.email localCollection.isPublic = fetchedCollection.isPublic ?? false localCollection.styleSheet = fetchedCollection.styleSheet localCollection.title = fetchedCollection.title localCollection.url = fetchedCollection.url } } DispatchQueue.main.async { LocalStorageManager().saveContext() } } catch WFError.unauthorized { DispatchQueue.main.async { self.loginErrorMessage = "Something went wrong, please try logging in again." self.isPresentingLoginErrorAlert = true } self.logout() } catch { print(error) } } func fetchUserPostsHandler(result: Result<[WFPost], Error>) { // We're done with the network request. DispatchQueue.main.async { self.isProcessingRequest = false } let request = WFAPost.createFetchRequest() do { let locallyCachedPosts = try LocalStorageManager.persistentContainer.viewContext.fetch(request) do { var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue } let fetchedPosts = try result.get() for fetchedPost in fetchedPosts { if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) { DispatchQueue.main.async { managedPost.wasDeletedFromServer = false if let fetchedPostUpdatedDate = fetchedPost.updatedDate, let localPostUpdatedDate = managedPost.updatedDate { managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate } else { print("Error: could not determine which copy of post is newer") } postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId }) } } else { DispatchQueue.main.async { let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) managedPost.postId = fetchedPost.postId managedPost.slug = fetchedPost.slug managedPost.appearance = fetchedPost.appearance managedPost.language = fetchedPost.language managedPost.rtl = fetchedPost.rtl ?? false managedPost.createdDate = fetchedPost.createdDate managedPost.updatedDate = fetchedPost.updatedDate managedPost.title = fetchedPost.title ?? "" managedPost.body = fetchedPost.body managedPost.collectionAlias = fetchedPost.collectionAlias managedPost.status = PostStatus.published.rawValue managedPost.wasDeletedFromServer = false } } } DispatchQueue.main.async { for post in postsToDelete { post.wasDeletedFromServer = true } LocalStorageManager().saveContext() } } catch { print(error) } } catch WFError.unauthorized { DispatchQueue.main.async { self.loginErrorMessage = "Something went wrong, please try logging in again." self.isPresentingLoginErrorAlert = true } self.logout() } catch { print("Error: Failed to fetch cached posts") } } func publishHandler(result: Result) { // We're done with the network request. DispatchQueue.main.async { self.isProcessingRequest = false } // ⚠️ NOTE: // The API does not return a collection alias, so we take care not to overwrite the // cached post's collection alias with the 'nil' value from the fetched post. // See: https://github.com/writeas/writefreely-swift/issues/20 do { let fetchedPost = try result.get() // If this is an updated post, check it against postToUpdate. if let updatingPost = self.postToUpdate { updatingPost.appearance = fetchedPost.appearance updatingPost.body = fetchedPost.body updatingPost.createdDate = fetchedPost.createdDate updatingPost.language = fetchedPost.language updatingPost.postId = fetchedPost.postId updatingPost.rtl = fetchedPost.rtl ?? false updatingPost.slug = fetchedPost.slug updatingPost.status = PostStatus.published.rawValue updatingPost.title = fetchedPost.title ?? "" updatingPost.updatedDate = fetchedPost.updatedDate DispatchQueue.main.async { LocalStorageManager().saveContext() } } else { // Otherwise if it's a newly-published post, find it in the local store. let request = WFAPost.createFetchRequest() let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body) if let fetchedPostTitle = fetchedPost.title { let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle) request.predicate = NSCompoundPredicate( andPredicateWithSubpredicates: [ matchTitlePredicate, matchBodyPredicate ] ) } else { request.predicate = matchBodyPredicate } do { let cachedPostsResults = try LocalStorageManager.persistentContainer.viewContext.fetch(request) guard let cachedPost = cachedPostsResults.first else { return } cachedPost.appearance = fetchedPost.appearance cachedPost.body = fetchedPost.body cachedPost.createdDate = fetchedPost.createdDate cachedPost.language = fetchedPost.language cachedPost.postId = fetchedPost.postId cachedPost.rtl = fetchedPost.rtl ?? false cachedPost.slug = fetchedPost.slug cachedPost.status = PostStatus.published.rawValue cachedPost.title = fetchedPost.title ?? "" cachedPost.updatedDate = fetchedPost.updatedDate DispatchQueue.main.async { LocalStorageManager().saveContext() } } catch { print("Error: Failed to fetch cached posts") } } } catch { print(error) } } func updateFromServerHandler(result: Result) { // We're done with the network request. DispatchQueue.main.async { self.isProcessingRequest = false } // ⚠️ NOTE: // The API does not return a collection alias, so we take care not to overwrite the // cached post's collection alias with the 'nil' value from the fetched post. // See: https://github.com/writeas/writefreely-swift/issues/20 do { let fetchedPost = try result.get() guard let cachedPost = self.selectedPost else { return } cachedPost.appearance = fetchedPost.appearance cachedPost.body = fetchedPost.body cachedPost.createdDate = fetchedPost.createdDate cachedPost.language = fetchedPost.language cachedPost.postId = fetchedPost.postId cachedPost.rtl = fetchedPost.rtl ?? false cachedPost.slug = fetchedPost.slug cachedPost.status = PostStatus.published.rawValue cachedPost.title = fetchedPost.title ?? "" cachedPost.updatedDate = fetchedPost.updatedDate cachedPost.hasNewerRemoteCopy = false DispatchQueue.main.async { LocalStorageManager().saveContext() } } catch { print(error) } } func movePostHandler(result: Result) { // We're done with the network request. DispatchQueue.main.async { self.isProcessingRequest = false } do { let succeeded = try result.get() if succeeded { if let post = selectedPost { updateFromServer(post: post) } else { return } } } catch { DispatchQueue.main.async { LocalStorageManager.persistentContainer.viewContext.rollback() } print(error) } } } - -private extension WriteFreelyModel { - // MARK: - Keychain Helpers - func saveTokenToKeychain(_ token: String, username: String?, server: String) { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecValueData as String: token.data(using: .utf8)!, - kSecAttrAccount as String: username ?? "anonymous", - kSecAttrService as String: server - ] - let status = SecItemAdd(query as CFDictionary, nil) - guard status == errSecDuplicateItem || status == errSecSuccess else { - fatalError("Error storing in Keychain with OSStatus: \(status)") - } - } - - func purgeTokenFromKeychain(username: String?, server: String) throws { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: username ?? "anonymous", - kSecAttrService as String: server - ] - let status = SecItemDelete(query as CFDictionary) - guard status == errSecSuccess || status == errSecItemNotFound else { - fatalError("Error deleting from Keychain with OSStatus: \(status)") - } - } - - func fetchTokenFromKeychain(username: String?, server: String) -> String? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: username ?? "anonymous", - kSecAttrService as String: server, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecReturnAttributes as String: true, - kSecReturnData as String: true - ] - var secItem: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &secItem) - guard status != errSecItemNotFound else { - return nil - } - guard status == errSecSuccess else { - fatalError("Error fetching from Keychain with OSStatus: \(status)") - } - guard let existingSecItem = secItem as? [String: Any], - let tokenData = existingSecItem[kSecValueData as String] as? Data, - let token = String(data: tokenData, encoding: .utf8) else { - return nil - } - return token - } -} diff --git a/Shared/Extensions/WriteFreelyModel+Keychain.swift b/Shared/Extensions/WriteFreelyModel+Keychain.swift new file mode 100644 index 0000000..fd37506 --- /dev/null +++ b/Shared/Extensions/WriteFreelyModel+Keychain.swift @@ -0,0 +1,53 @@ +import Foundation + +extension WriteFreelyModel { + func saveTokenToKeychain(_ token: String, username: String?, server: String) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecValueData as String: token.data(using: .utf8)!, + kSecAttrAccount as String: username ?? "anonymous", + kSecAttrService as String: server + ] + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecDuplicateItem || status == errSecSuccess else { + fatalError("Error storing in Keychain with OSStatus: \(status)") + } + } + + func purgeTokenFromKeychain(username: String?, server: String) throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: username ?? "anonymous", + kSecAttrService as String: server + ] + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + fatalError("Error deleting from Keychain with OSStatus: \(status)") + } + } + + func fetchTokenFromKeychain(username: String?, server: String) -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: username ?? "anonymous", + kSecAttrService as String: server, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnAttributes as String: true, + kSecReturnData as String: true + ] + var secItem: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &secItem) + guard status != errSecItemNotFound else { + return nil + } + guard status == errSecSuccess else { + fatalError("Error fetching from Keychain with OSStatus: \(status)") + } + guard let existingSecItem = secItem as? [String: Any], + let tokenData = existingSecItem[kSecValueData as String] as? Data, + let token = String(data: tokenData, encoding: .utf8) else { + return nil + } + return token + } +} diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 286f39b..3c9c903 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -1,571 +1,74 @@ import Foundation import WriteFreely import Security import Network // MARK: - WriteFreelyModel class WriteFreelyModel: ObservableObject { @Published var account = AccountModel() @Published var preferences = PreferencesModel() @Published var posts = PostListModel() @Published var editor = PostEditorModel() @Published var isLoggingIn: Bool = false @Published var isProcessingRequest: Bool = false @Published var hasNetworkConnection: Bool = true @Published var selectedPost: WFAPost? @Published var selectedCollection: WFACollection? @Published var showAllPosts: Bool = true @Published var isPresentingDeleteAlert: Bool = false @Published var isPresentingLoginErrorAlert: Bool = false @Published var isPresentingNetworkErrorAlert: Bool = false @Published var postToDelete: WFAPost? #if os(iOS) @Published var isPresentingSettingsView: Bool = false #endif var loginErrorMessage: String? // swiftlint:disable line_length let helpURL = URL(string: "https://discuss.write.as/c/help/5")! let howToURL = URL(string: "https://discuss.write.as/t/using-the-writefreely-ios-app/1946")! let reviewURL = URL(string: "https://apps.apple.com/app/id1531530896?action=write-review")! let licensesURL = URL(string: "https://github.com/writeas/writefreely-swiftui-multiplatform/tree/main/Shared/Resources/Licenses")! // swiftlint:enable line_length - private var client: WFClient? + internal var client: WFClient? private let defaults = UserDefaults.standard private let monitor = NWPathMonitor() private let queue = DispatchQueue(label: "NetworkMonitor") - private var postToUpdate: WFAPost? + internal var postToUpdate: WFAPost? init() { DispatchQueue.main.async { self.preferences.appearance = self.defaults.integer(forKey: self.preferences.colorSchemeIntegerKey) self.preferences.font = self.defaults.integer(forKey: self.preferences.defaultFontIntegerKey) self.account.restoreState() if self.account.isLoggedIn { guard let serverURL = URL(string: self.account.server) else { print("Server URL not found") return } guard let token = self.fetchTokenFromKeychain( username: self.account.username, server: self.account.server ) else { print("Could not fetch token from Keychain") return } self.account.login(WFUser(token: token, username: self.account.username)) self.client = WFClient(for: serverURL) self.client?.user = self.account.user self.fetchUserCollections() self.fetchUserPosts() } } monitor.pathUpdateHandler = { path in DispatchQueue.main.async { self.hasNetworkConnection = path.status == .satisfied } } monitor.start(queue: queue) } } - -// MARK: - WriteFreelyModel API - -extension WriteFreelyModel { - func login(to server: URL, as username: String, password: String) { - if !hasNetworkConnection { - isPresentingNetworkErrorAlert = true - return - } - let secureProtocolPrefix = "https://" - let insecureProtocolPrefix = "http://" - var serverString = server.absoluteString - // If there's neither an http or https prefix, prepend "https://" to the server string. - if !(serverString.hasPrefix(secureProtocolPrefix) || serverString.hasPrefix(insecureProtocolPrefix)) { - serverString = secureProtocolPrefix + serverString - } - // If the server string is prefixed with http, upgrade to https before attempting to login. - if serverString.hasPrefix(insecureProtocolPrefix) { - serverString = serverString.replacingOccurrences(of: insecureProtocolPrefix, with: secureProtocolPrefix) - } - isLoggingIn = true - var serverURL = URL(string: serverString)! - if !serverURL.path.isEmpty { - serverURL.deleteLastPathComponent() - } - account.server = serverURL.absoluteString - client = WFClient(for: serverURL) - client?.login(username: username, password: password, completion: loginHandler) - } - - func logout() { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { - do { - try purgeTokenFromKeychain(username: account.username, server: account.server) - account.logout() - } catch { - fatalError("Failed to log out persisted state") - } - return - } - loggedInClient.logout(completion: logoutHandler) - } - - func fetchUserCollections() { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler) - } - - func fetchUserPosts() { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - loggedInClient.getPosts(completion: fetchUserPostsHandler) - } - - func publish(post: WFAPost) { - postToUpdate = nil - - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - - if post.language == nil { - if let languageCode = Locale.current.languageCode { - post.language = languageCode - post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft - } - } - - var wfPost = WFPost( - body: post.body, - title: post.title.isEmpty ? "" : post.title, - appearance: post.appearance, - language: post.language, - rtl: post.rtl, - createdDate: post.createdDate - ) - - if let existingPostId = post.postId { - // This is an existing post. - postToUpdate = post - wfPost.postId = post.postId - - loggedInClient.updatePost( - postId: existingPostId, - updatedPost: wfPost, - completion: publishHandler - ) - } else { - // This is a new local draft. - loggedInClient.createPost( - post: wfPost, in: post.collectionAlias, completion: publishHandler - ) - } - } - - func updateFromServer(post: WFAPost) { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client else { return } - guard let postId = post.postId else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.selectedPost = post - self.isProcessingRequest = true - } - loggedInClient.getPost(byId: postId, completion: updateFromServerHandler) - } - - func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) { - if !hasNetworkConnection { - DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true } - return - } - guard let loggedInClient = client, - let postId = post.postId else { return } - // We're starting the network request. - DispatchQueue.main.async { - self.isProcessingRequest = true - } - - selectedPost = post - post.collectionAlias = newCollection?.alias - loggedInClient.movePost(postId: postId, to: newCollection?.alias, completion: movePostHandler) - } -} - -private extension WriteFreelyModel { - func loginHandler(result: Result) { - DispatchQueue.main.async { - self.isLoggingIn = false - } - do { - let user = try result.get() - fetchUserCollections() - fetchUserPosts() - saveTokenToKeychain(user.token, username: user.username, server: account.server) - DispatchQueue.main.async { - self.account.login(user) - } - } catch WFError.notFound { - DispatchQueue.main.async { - self.loginErrorMessage = AccountError.usernameNotFound.localizedDescription - self.isPresentingLoginErrorAlert = true - } - } catch WFError.unauthorized { - DispatchQueue.main.async { - self.loginErrorMessage = AccountError.invalidPassword.localizedDescription - self.isPresentingLoginErrorAlert = true - } - } catch { - if (error as NSError).domain == NSURLErrorDomain, - (error as NSError).code == -1003 { - DispatchQueue.main.async { - self.loginErrorMessage = AccountError.serverNotFound.localizedDescription - self.isPresentingLoginErrorAlert = true - } - } else { - DispatchQueue.main.async { - self.loginErrorMessage = error.localizedDescription - self.isPresentingLoginErrorAlert = true - } - } - } - } - - func logoutHandler(result: Result) { - do { - _ = try result.get() - do { - try purgeTokenFromKeychain(username: account.user?.username, server: account.server) - client = nil - DispatchQueue.main.async { - self.account.logout() - LocalStorageManager().purgeUserCollections() - self.posts.purgePublishedPosts() - } - } catch { - print("Something went wrong purging the token from the Keychain.") - } - } catch WFError.notFound { - // The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with - // purging the token from the Keychain, destroying the client object, and setting the AccountModel to its - // logged-out state. - do { - try purgeTokenFromKeychain(username: account.user?.username, server: account.server) - client = nil - DispatchQueue.main.async { - self.account.logout() - LocalStorageManager().purgeUserCollections() - self.posts.purgePublishedPosts() - } - } catch { - print("Something went wrong purging the token from the Keychain.") - } - } catch { - // We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here, - // so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're - // logged in, try calling the logout function again and see what we get. - // Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties. - if (error as NSError).domain == NSURLErrorDomain, - (error as NSError).code == NSURLErrorCannotParseResponse { - if account.isLoggedIn { - self.logout() - } - } - } - } - - func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) { - // We're done with the network request. - DispatchQueue.main.async { - self.isProcessingRequest = false - } - do { - let fetchedCollections = try result.get() - for fetchedCollection in fetchedCollections { - DispatchQueue.main.async { - let localCollection = WFACollection(context: LocalStorageManager.persistentContainer.viewContext) - localCollection.alias = fetchedCollection.alias - localCollection.blogDescription = fetchedCollection.description - localCollection.email = fetchedCollection.email - localCollection.isPublic = fetchedCollection.isPublic ?? false - localCollection.styleSheet = fetchedCollection.styleSheet - localCollection.title = fetchedCollection.title - localCollection.url = fetchedCollection.url - } - } - DispatchQueue.main.async { - LocalStorageManager().saveContext() - } - } catch WFError.unauthorized { - DispatchQueue.main.async { - self.loginErrorMessage = "Something went wrong, please try logging in again." - self.isPresentingLoginErrorAlert = true - } - self.logout() - } catch { - print(error) - } - } - - func fetchUserPostsHandler(result: Result<[WFPost], Error>) { - // We're done with the network request. - DispatchQueue.main.async { - self.isProcessingRequest = false - } - let request = WFAPost.createFetchRequest() - do { - let locallyCachedPosts = try LocalStorageManager.persistentContainer.viewContext.fetch(request) - do { - var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue } - let fetchedPosts = try result.get() - for fetchedPost in fetchedPosts { - if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) { - DispatchQueue.main.async { - managedPost.wasDeletedFromServer = false - if let fetchedPostUpdatedDate = fetchedPost.updatedDate, - let localPostUpdatedDate = managedPost.updatedDate { - managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate - } else { print("Error: could not determine which copy of post is newer") } - postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId }) - } - } else { - DispatchQueue.main.async { - let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) - managedPost.postId = fetchedPost.postId - managedPost.slug = fetchedPost.slug - managedPost.appearance = fetchedPost.appearance - managedPost.language = fetchedPost.language - managedPost.rtl = fetchedPost.rtl ?? false - managedPost.createdDate = fetchedPost.createdDate - managedPost.updatedDate = fetchedPost.updatedDate - managedPost.title = fetchedPost.title ?? "" - managedPost.body = fetchedPost.body - managedPost.collectionAlias = fetchedPost.collectionAlias - managedPost.status = PostStatus.published.rawValue - managedPost.wasDeletedFromServer = false - } - } - } - DispatchQueue.main.async { - for post in postsToDelete { post.wasDeletedFromServer = true } - LocalStorageManager().saveContext() - } - } catch { - print(error) - } - } catch WFError.unauthorized { - DispatchQueue.main.async { - self.loginErrorMessage = "Something went wrong, please try logging in again." - self.isPresentingLoginErrorAlert = true - } - self.logout() - } catch { - print("Error: Failed to fetch cached posts") - } - } - - func publishHandler(result: Result) { - // We're done with the network request. - DispatchQueue.main.async { - self.isProcessingRequest = false - } - // ⚠️ NOTE: - // The API does not return a collection alias, so we take care not to overwrite the - // cached post's collection alias with the 'nil' value from the fetched post. - // See: https://github.com/writeas/writefreely-swift/issues/20 - do { - let fetchedPost = try result.get() - // If this is an updated post, check it against postToUpdate. - if let updatingPost = self.postToUpdate { - updatingPost.appearance = fetchedPost.appearance - updatingPost.body = fetchedPost.body - updatingPost.createdDate = fetchedPost.createdDate - updatingPost.language = fetchedPost.language - updatingPost.postId = fetchedPost.postId - updatingPost.rtl = fetchedPost.rtl ?? false - updatingPost.slug = fetchedPost.slug - updatingPost.status = PostStatus.published.rawValue - updatingPost.title = fetchedPost.title ?? "" - updatingPost.updatedDate = fetchedPost.updatedDate - DispatchQueue.main.async { - LocalStorageManager().saveContext() - } - } else { - // Otherwise if it's a newly-published post, find it in the local store. - let request = WFAPost.createFetchRequest() - let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body) - if let fetchedPostTitle = fetchedPost.title { - let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle) - request.predicate = NSCompoundPredicate( - andPredicateWithSubpredicates: [ - matchTitlePredicate, - matchBodyPredicate - ] - ) - } else { - request.predicate = matchBodyPredicate - } - do { - let cachedPostsResults = try LocalStorageManager.persistentContainer.viewContext.fetch(request) - guard let cachedPost = cachedPostsResults.first else { return } - cachedPost.appearance = fetchedPost.appearance - cachedPost.body = fetchedPost.body - cachedPost.createdDate = fetchedPost.createdDate - cachedPost.language = fetchedPost.language - cachedPost.postId = fetchedPost.postId - cachedPost.rtl = fetchedPost.rtl ?? false - cachedPost.slug = fetchedPost.slug - cachedPost.status = PostStatus.published.rawValue - cachedPost.title = fetchedPost.title ?? "" - cachedPost.updatedDate = fetchedPost.updatedDate - DispatchQueue.main.async { - LocalStorageManager().saveContext() - } - } catch { - print("Error: Failed to fetch cached posts") - } - } - } catch { - print(error) - } - } - - func updateFromServerHandler(result: Result) { - // We're done with the network request. - DispatchQueue.main.async { - self.isProcessingRequest = false - } - // ⚠️ NOTE: - // The API does not return a collection alias, so we take care not to overwrite the - // cached post's collection alias with the 'nil' value from the fetched post. - // See: https://github.com/writeas/writefreely-swift/issues/20 - do { - let fetchedPost = try result.get() - guard let cachedPost = self.selectedPost else { return } - cachedPost.appearance = fetchedPost.appearance - cachedPost.body = fetchedPost.body - cachedPost.createdDate = fetchedPost.createdDate - cachedPost.language = fetchedPost.language - cachedPost.postId = fetchedPost.postId - cachedPost.rtl = fetchedPost.rtl ?? false - cachedPost.slug = fetchedPost.slug - cachedPost.status = PostStatus.published.rawValue - cachedPost.title = fetchedPost.title ?? "" - cachedPost.updatedDate = fetchedPost.updatedDate - cachedPost.hasNewerRemoteCopy = false - DispatchQueue.main.async { - LocalStorageManager().saveContext() - } - } catch { - print(error) - } - } - - func movePostHandler(result: Result) { - // We're done with the network request. - DispatchQueue.main.async { - self.isProcessingRequest = false - } - do { - let succeeded = try result.get() - if succeeded { - if let post = selectedPost { - updateFromServer(post: post) - } else { - return - } - } - } catch { - DispatchQueue.main.async { - LocalStorageManager.persistentContainer.viewContext.rollback() - } - print(error) - } - } -} - -private extension WriteFreelyModel { - // MARK: - Keychain Helpers - func saveTokenToKeychain(_ token: String, username: String?, server: String) { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecValueData as String: token.data(using: .utf8)!, - kSecAttrAccount as String: username ?? "anonymous", - kSecAttrService as String: server - ] - let status = SecItemAdd(query as CFDictionary, nil) - guard status == errSecDuplicateItem || status == errSecSuccess else { - fatalError("Error storing in Keychain with OSStatus: \(status)") - } - } - - func purgeTokenFromKeychain(username: String?, server: String) throws { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: username ?? "anonymous", - kSecAttrService as String: server - ] - let status = SecItemDelete(query as CFDictionary) - guard status == errSecSuccess || status == errSecItemNotFound else { - fatalError("Error deleting from Keychain with OSStatus: \(status)") - } - } - - func fetchTokenFromKeychain(username: String?, server: String) -> String? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: username ?? "anonymous", - kSecAttrService as String: server, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecReturnAttributes as String: true, - kSecReturnData as String: true - ] - var secItem: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &secItem) - guard status != errSecItemNotFound else { - return nil - } - guard status == errSecSuccess else { - fatalError("Error fetching from Keychain with OSStatus: \(status)") - } - guard let existingSecItem = secItem as? [String: Any], - let tokenData = existingSecItem[kSecValueData as String] as? Data, - let token = String(data: tokenData, encoding: .utf8) else { - return nil - } - return token - } -} diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 2a02539..138793a 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -1,1239 +1,1257 @@ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */; }; 170DFA35251BBC44001D82A0 /* PostEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */; }; 17120DA124E19839002B9F6C /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; }; 17120DA224E1985C002B9F6C /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */; }; 17120DA324E19A42002B9F6C /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; }; 17120DA724E19D11002B9F6C /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DA424E19CBF002B9F6C /* SettingsView.swift */; }; 17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */; }; 17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */; }; 17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */; }; 17120DAD24E1B99F002B9F6C /* AccountLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */; }; 17120DB224E1E19C002B9F6C /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */; }; 171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BFDF924D4AF8300888236 /* CollectionListView.swift */; }; 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BFDF924D4AF8300888236 /* CollectionListView.swift */; }; 172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172C492D2593981900E20ADF /* MacUpdatesView.swift */; }; 173E19D1254318F600440F0F /* RemoteChangePromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173E19D0254318F600440F0F /* RemoteChangePromptView.swift */; }; 173E19E3254329CC00440F0F /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173E19E2254329CC00440F0F /* PostTextEditingView.swift */; }; 17466626256C0D0600629997 /* MacEditorTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17466625256C0D0600629997 /* MacEditorTextView.swift */; }; 17479F152583D8E40072B7FB /* PostEditorSharingPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17479F142583D8E40072B7FB /* PostEditorSharingPicker.swift */; }; 17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */; }; 17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */; }; 174D313224EC2831006CA9EE /* WriteFreelyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */; }; 174D313324EC2831006CA9EE /* WriteFreelyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */; }; 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1753F6AB24E431CC00309365 /* MacPreferencesView.swift */; }; 1756AE6E24CB255B00FD7257 /* PostListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6D24CB255B00FD7257 /* PostListModel.swift */; }; 1756AE6F24CB255B00FD7257 /* PostListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6D24CB255B00FD7257 /* PostListModel.swift */; }; 1756AE7424CB26FA00FD7257 /* PostCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7324CB26FA00FD7257 /* PostCellView.swift */; }; 1756AE7524CB26FA00FD7257 /* PostCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7324CB26FA00FD7257 /* PostCellView.swift */; }; 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */; }; 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostListView.swift */; }; 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostListView.swift */; }; 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE8024CB844500FD7257 /* View+Keyboard.swift */; }; 1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */; }; 1756DBB424FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */; }; 1756DBB724FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */; }; 1756DBB824FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */; }; 1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */; }; 1756DBBB24FED45500207AB8 /* LocalStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */; }; 1756DC0124FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */; }; 1756DC0224FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */; }; 1756DC0324FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */; }; 1756DC0424FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */; }; 17681E412519410E00D394AE /* UINavigationController+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17681E402519410E00D394AE /* UINavigationController+Appearance.swift */; }; 1780F6EF25895EDB00FE45FF /* PostCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1780F6EE25895EDB00FE45FF /* PostCommands.swift */; }; 17A4FEDA25924AF70037E96B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 17A4FED925924AF70037E96B /* Sparkle */; }; 17A4FEED25927E730037E96B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A4FEEC25927E730037E96B /* AppDelegate.swift */; }; 17A5388824DDA31F00DEFF9A /* MacAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388724DDA31F00DEFF9A /* MacAccountView.swift */; }; 17A5388C24DDC83F00DEFF9A /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */; }; 17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; }; 17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; }; 17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A67CAE251A5DD7002F163D /* PostEditorView.swift */; }; 17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */; }; 17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A6325489E900057D763 /* PostBodyTextView.swift */; }; + 17B37C4B25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */; }; + 17B37C4C25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */; }; + 17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */; }; + 17B37C5725C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */; }; + 17B37C5D25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C5C25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift */; }; + 17B37C5E25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C5C25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift */; }; 17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */; }; 17B5103B2515448D00E9631F /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17B5103A2515448D00E9631F /* Credits.rtf */; }; 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; }; 17B996D92502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; }; 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */; }; 17B996DB2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */; }; 17BC618A25715318003363CA /* ActivePostToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC617825715068003363CA /* ActivePostToolbarView.swift */; }; 17C42E622507D8E600072984 /* PostStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E612507D8E600072984 /* PostStatus.swift */; }; 17C42E632507D8E600072984 /* PostStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E612507D8E600072984 /* PostStatus.swift */; }; 17C42E652509237800072984 /* PostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E642509237800072984 /* PostListFilteredView.swift */; }; 17C42E662509237800072984 /* PostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E642509237800072984 /* PostListFilteredView.swift */; }; 17C42E70250AA12300072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D435E724E3128F0036B539 /* PreferencesModel.swift */; }; 17D435E924E3128F0036B539 /* PreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D435E724E3128F0036B539 /* PreferencesModel.swift */; }; 17D4F36C2514EE2F00517CE6 /* LoraGX.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */; }; 17D4F36D2514EE2F00517CE6 /* LoraGX.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */; }; 17D4F39E2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */; }; 17D4F39F2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */; }; 17D4F3A52514F1E900517CE6 /* Hack-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */; }; 17D4F3A62514F1E900517CE6 /* Hack-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */; }; 17DF329D24C87D3500BCE2E3 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF329C24C87D3500BCE2E3 /* Tests_iOS.swift */; }; 17DF32A824C87D3500BCE2E3 /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32A724C87D3500BCE2E3 /* Tests_macOS.swift */; }; 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */; }; 17DF32AB24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */; }; 17DF32AC24C87D3500BCE2E3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328224C87D3300BCE2E3 /* ContentView.swift */; }; 17DF32AD24C87D3500BCE2E3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328224C87D3300BCE2E3 /* ContentView.swift */; }; 17DF32AE24C87D3500BCE2E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17DF328324C87D3500BCE2E3 /* Assets.xcassets */; }; 17DF32AF24C87D3500BCE2E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17DF328324C87D3500BCE2E3 /* Assets.xcassets */; }; 17DF32C024C87D7B00BCE2E3 /* WriteFreely in Frameworks */ = {isa = PBXBuildFile; productRef = 17DF32BF24C87D7B00BCE2E3 /* WriteFreely */; }; 17DF32C324C87D8D00BCE2E3 /* WriteFreely in Frameworks */ = {isa = PBXBuildFile; productRef = 17DF32C224C87D8D00BCE2E3 /* WriteFreely */; }; 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */; }; 17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */; }; 17DFDE87251D309400A25F31 /* Hack-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE84251D309400A25F31 /* Hack-License.txt */; }; 17DFDE88251D309400A25F31 /* Hack-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE84251D309400A25F31 /* Hack-License.txt */; }; 17DFDE89251D309400A25F31 /* Lora-Cyrillic-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE85251D309400A25F31 /* Lora-Cyrillic-OFL.txt */; }; 17DFDE8A251D309400A25F31 /* Lora-Cyrillic-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE85251D309400A25F31 /* Lora-Cyrillic-OFL.txt */; }; 17DFDE8B251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; }; 17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; }; 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 17DF329924C87D3500BCE2E3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 17DF327C24C87D3300BCE2E3 /* Project object */; proxyType = 1; remoteGlobalIDString = 17DF328724C87D3500BCE2E3; remoteInfo = "WriteFreely-MultiPlatform (iOS)"; }; 17DF32A424C87D3500BCE2E3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 17DF327C24C87D3300BCE2E3 /* Project object */; proxyType = 1; remoteGlobalIDString = 17DF328F24C87D3500BCE2E3; remoteInfo = "WriteFreely-MultiPlatform (macOS)"; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 1709ADDF251B9A110053AF79 /* EditorLaunchingPolicy.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = EditorLaunchingPolicy.md; sourceTree = ""; }; 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorModel.swift; sourceTree = ""; }; 17120DA424E19CBF002B9F6C /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLogoutView.swift; sourceTree = ""; }; 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLoginView.swift; sourceTree = ""; }; 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = ""; }; 171BFDF924D4AF8300888236 /* CollectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionListView.swift; sourceTree = ""; }; 172C492D2593981900E20ADF /* MacUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacUpdatesView.swift; sourceTree = ""; }; 173E19D0254318F600440F0F /* RemoteChangePromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteChangePromptView.swift; sourceTree = ""; }; 173E19E2254329CC00440F0F /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = ""; }; 17466625256C0D0600629997 /* MacEditorTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacEditorTextView.swift; sourceTree = ""; }; 17479F142583D8E40072B7FB /* PostEditorSharingPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorSharingPicker.swift; sourceTree = ""; }; 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppVersion.swift"; sourceTree = ""; }; 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteFreelyModel.swift; sourceTree = ""; }; 1753F6AB24E431CC00309365 /* MacPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPreferencesView.swift; sourceTree = ""; }; 1756AE6D24CB255B00FD7257 /* PostListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListModel.swift; sourceTree = ""; }; 1756AE7324CB26FA00FD7257 /* PostCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCellView.swift; sourceTree = ""; }; 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = ""; }; 1756AE7924CB65DF00FD7257 /* PostListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListView.swift; sourceTree = ""; }; 1756AE8024CB844500FD7257 /* View+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Keyboard.swift"; sourceTree = ""; }; 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorStatusToolbarView.swift; sourceTree = ""; }; 1756DBB624FED3A400207AB8 /* LocalStorageModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LocalStorageModel.xcdatamodel; sourceTree = ""; }; 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStorageManager.swift; sourceTree = ""; }; 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFACollection+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFACollection+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; 17681E402519410E00D394AE /* UINavigationController+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Appearance.swift"; sourceTree = ""; }; 1780F6EE25895EDB00FE45FF /* PostCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCommands.swift; sourceTree = ""; }; 17A4FEDF25924E810037E96B /* MacSoftwareUpdater.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = MacSoftwareUpdater.md; sourceTree = ""; }; 17A4FEEC25927E730037E96B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 17A5388724DDA31F00DEFF9A /* MacAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAccountView.swift; sourceTree = ""; }; 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountModel.swift; sourceTree = ""; }; 17A5388D24DDEC7400DEFF9A /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; 17A5389124DDED0000DEFF9A /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; 17A67CAE251A5DD7002F163D /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = ""; }; 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTitleTextView.swift; sourceTree = ""; }; 17AD0A6325489E900057D763 /* PostBodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBodyTextView.swift; sourceTree = ""; }; + 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+Keychain.swift"; sourceTree = ""; }; + 17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+API.swift"; sourceTree = ""; }; + 17B37C5C25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+APIHandlers.swift"; sourceTree = ""; }; 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 17B5103A2515448D00E9631F /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 17B68D4F25A4FED2005ED37C /* Sparkle-License.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Sparkle-License.txt"; sourceTree = ""; }; 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; 17BC617825715068003363CA /* ActivePostToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePostToolbarView.swift; sourceTree = ""; }; 17C42E612507D8E600072984 /* PostStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStatus.swift; sourceTree = ""; }; 17C42E642509237800072984 /* PostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListFilteredView.swift; sourceTree = ""; }; 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+ExecuteAndMergeChanges.swift"; sourceTree = ""; }; 17D435E724E3128F0036B539 /* PreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesModel.swift; sourceTree = ""; }; 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = LoraGX.ttf; sourceTree = ""; }; 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Regular.ttf"; sourceTree = ""; }; 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Hack-Regular.ttf"; sourceTree = ""; }; 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteFreely_MultiPlatformApp.swift; sourceTree = ""; }; 17DF328224C87D3300BCE2E3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 17DF328324C87D3500BCE2E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 17DF328824C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WriteFreely-MultiPlatform.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF328B24C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF329024C87D3500BCE2E3 /* WriteFreely for Mac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WriteFreely for Mac.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF329224C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF329324C87D3500BCE2E3 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 17DF329824C87D3500BCE2E3 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF329C24C87D3500BCE2E3 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; }; 17DF329E24C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF32A324C87D3500BCE2E3 /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF32A724C87D3500BCE2E3 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = ""; }; 17DF32A924C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF32C624C884FF00BCE2E3 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 17DF32C724C8853700BCE2E3 /* CODE_OF_CONDUCT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CODE_OF_CONDUCT.md; sourceTree = ""; }; 17DF32C824C8854B00BCE2E3 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; 17DF32C924C8855E00BCE2E3 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 17DF32CA24C8856C00BCE2E3 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStatusBadgeView.swift; sourceTree = ""; }; 17DFDE84251D309400A25F31 /* Hack-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Hack-License.txt"; sourceTree = ""; }; 17DFDE85251D309400A25F31 /* Lora-Cyrillic-OFL.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Lora-Cyrillic-OFL.txt"; sourceTree = ""; }; 17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = ""; }; 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 17DF328524C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 17DF32C024C87D7B00BCE2E3 /* WriteFreely in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF328D24C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 17DF32C324C87D8D00BCE2E3 /* WriteFreely in Frameworks */, 17A4FEDA25924AF70037E96B /* Sparkle in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329524C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 17DF32A024C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 1709ADDE251B99D40053AF79 /* Technotes */ = { isa = PBXGroup; children = ( 1709ADDF251B9A110053AF79 /* EditorLaunchingPolicy.md */, 17A4FEDF25924E810037E96B /* MacSoftwareUpdater.md */, ); path = Technotes; sourceTree = ""; }; 17120DA624E19CE2002B9F6C /* Settings */ = { isa = PBXGroup; children = ( 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */, 17120DA424E19CBF002B9F6C /* SettingsView.swift */, ); path = Settings; sourceTree = ""; }; 1739B8D324EAFAB700DA7421 /* PostEditor */ = { isa = PBXGroup; children = ( 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */, 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */, ); path = PostEditor; sourceTree = ""; }; 1756AE7F24CB841200FD7257 /* Extensions */ = { isa = PBXGroup; children = ( 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */, 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */, + 17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */, + 17B37C5C25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift */, + 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */, ); path = Extensions; sourceTree = ""; }; 1762DCB124EB07680019C4EB /* Models */ = { isa = PBXGroup; children = ( 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */, 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */, 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */, 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */, 17C42E612507D8E600072984 /* PostStatus.swift */, 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */, 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */, ); path = Models; sourceTree = ""; }; 1765F62C24E1924800C9EBF0 /* Preferences */ = { isa = PBXGroup; children = ( 17D435E724E3128F0036B539 /* PreferencesModel.swift */, 17A5389124DDED0000DEFF9A /* PreferencesView.swift */, ); path = Preferences; sourceTree = ""; }; 17681E3F251940F200D394AE /* Extensions */ = { isa = PBXGroup; children = ( 1756AE8024CB844500FD7257 /* View+Keyboard.swift */, 17681E402519410E00D394AE /* UINavigationController+Appearance.swift */, ); path = Extensions; sourceTree = ""; }; 17A5388924DDA50500DEFF9A /* Settings */ = { isa = PBXGroup; children = ( 17A5388724DDA31F00DEFF9A /* MacAccountView.swift */, 1753F6AB24E431CC00309365 /* MacPreferencesView.swift */, 172C492D2593981900E20ADF /* MacUpdatesView.swift */, ); path = Settings; sourceTree = ""; }; 17A67CAB251A5D7E002F163D /* PostEditor */ = { isa = PBXGroup; children = ( 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */, 173E19D0254318F600440F0F /* RemoteChangePromptView.swift */, 173E19E2254329CC00440F0F /* PostTextEditingView.swift */, 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */, 17AD0A6325489E900057D763 /* PostBodyTextView.swift */, ); path = PostEditor; sourceTree = ""; }; 17A67CAC251A5D8D002F163D /* PostEditor */ = { isa = PBXGroup; children = ( 17479F142583D8E40072B7FB /* PostEditorSharingPicker.swift */, 17A67CAE251A5DD7002F163D /* PostEditorView.swift */, 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */, 17466625256C0D0600629997 /* MacEditorTextView.swift */, ); path = PostEditor; sourceTree = ""; }; 17BC617725715042003363CA /* Navigation */ = { isa = PBXGroup; children = ( 17BC617825715068003363CA /* ActivePostToolbarView.swift */, 1780F6EE25895EDB00FE45FF /* PostCommands.swift */, ); path = Navigation; sourceTree = ""; }; 17D4F3722514EE4400517CE6 /* Resources */ = { isa = PBXGroup; children = ( 17DFDE83251D309400A25F31 /* Licenses */, 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */, 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */, 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */, ); path = Resources; sourceTree = ""; }; 17DF327B24C87D3300BCE2E3 = { isa = PBXGroup; children = ( 17DF32C624C884FF00BCE2E3 /* README.md */, 17DF32C924C8855E00BCE2E3 /* LICENSE.md */, 17DF32CA24C8856C00BCE2E3 /* CHANGELOG.md */, 17DF32C724C8853700BCE2E3 /* CODE_OF_CONDUCT.md */, 17DF32C824C8854B00BCE2E3 /* CONTRIBUTING.md */, 1709ADDE251B99D40053AF79 /* Technotes */, 17DF328024C87D3300BCE2E3 /* Shared */, 17DF328A24C87D3500BCE2E3 /* iOS */, 17DF329124C87D3500BCE2E3 /* macOS */, 17DF329B24C87D3500BCE2E3 /* Tests iOS */, 17DF32A624C87D3500BCE2E3 /* Tests macOS */, 17DF328924C87D3500BCE2E3 /* Products */, 17DF32C124C87D8D00BCE2E3 /* Frameworks */, ); sourceTree = ""; }; 17DF328024C87D3300BCE2E3 /* Shared */ = { isa = PBXGroup; children = ( 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */, 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */, 17DF328324C87D3500BCE2E3 /* Assets.xcassets */, 17DF32D024C8B75C00BCE2E3 /* Account */, 1756AE7F24CB841200FD7257 /* Extensions */, 1762DCB124EB07680019C4EB /* Models */, 17DF32CC24C8B72300BCE2E3 /* Navigation */, 1739B8D324EAFAB700DA7421 /* PostEditor */, 17DF32D124C8B78500BCE2E3 /* PostList */, 17DF32D224C8B78D00BCE2E3 /* PostCollection */, 1765F62C24E1924800C9EBF0 /* Preferences */, 17D4F3722514EE4400517CE6 /* Resources */, ); path = Shared; sourceTree = ""; }; 17DF328924C87D3500BCE2E3 /* Products */ = { isa = PBXGroup; children = ( 17DF328824C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */, 17DF329024C87D3500BCE2E3 /* WriteFreely for Mac.app */, 17DF329824C87D3500BCE2E3 /* Tests iOS.xctest */, 17DF32A324C87D3500BCE2E3 /* Tests macOS.xctest */, ); name = Products; sourceTree = ""; }; 17DF328A24C87D3500BCE2E3 /* iOS */ = { isa = PBXGroup; children = ( 17DF328B24C87D3500BCE2E3 /* Info.plist */, 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */, 17681E3F251940F200D394AE /* Extensions */, 17A67CAB251A5D7E002F163D /* PostEditor */, 17120DA624E19CE2002B9F6C /* Settings */, ); path = iOS; sourceTree = ""; }; 17DF329124C87D3500BCE2E3 /* macOS */ = { isa = PBXGroup; children = ( 17DF329224C87D3500BCE2E3 /* Info.plist */, 17DF329324C87D3500BCE2E3 /* macOS.entitlements */, 17A4FEEC25927E730037E96B /* AppDelegate.swift */, 17BC617725715042003363CA /* Navigation */, 17A67CAC251A5D8D002F163D /* PostEditor */, 17A5388924DDA50500DEFF9A /* Settings */, 17B5103A2515448D00E9631F /* Credits.rtf */, ); path = macOS; sourceTree = ""; }; 17DF329B24C87D3500BCE2E3 /* Tests iOS */ = { isa = PBXGroup; children = ( 17DF329C24C87D3500BCE2E3 /* Tests_iOS.swift */, 17DF329E24C87D3500BCE2E3 /* Info.plist */, ); path = "Tests iOS"; sourceTree = ""; }; 17DF32A624C87D3500BCE2E3 /* Tests macOS */ = { isa = PBXGroup; children = ( 17DF32A724C87D3500BCE2E3 /* Tests_macOS.swift */, 17DF32A924C87D3500BCE2E3 /* Info.plist */, ); path = "Tests macOS"; sourceTree = ""; }; 17DF32C124C87D8D00BCE2E3 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 17DF32CC24C8B72300BCE2E3 /* Navigation */ = { isa = PBXGroup; children = ( 17DF328224C87D3300BCE2E3 /* ContentView.swift */, ); path = Navigation; sourceTree = ""; }; 17DF32D024C8B75C00BCE2E3 /* Account */ = { isa = PBXGroup; children = ( 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */, 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */, 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */, 17A5388D24DDEC7400DEFF9A /* AccountView.swift */, ); path = Account; sourceTree = ""; }; 17DF32D124C8B78500BCE2E3 /* PostList */ = { isa = PBXGroup; children = ( 1756AE7324CB26FA00FD7257 /* PostCellView.swift */, 1756AE6D24CB255B00FD7257 /* PostListModel.swift */, 1756AE7924CB65DF00FD7257 /* PostListView.swift */, 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */, 17C42E642509237800072984 /* PostListFilteredView.swift */, ); path = PostList; sourceTree = ""; }; 17DF32D224C8B78D00BCE2E3 /* PostCollection */ = { isa = PBXGroup; children = ( 171BFDF924D4AF8300888236 /* CollectionListView.swift */, ); path = PostCollection; sourceTree = ""; }; 17DFDE83251D309400A25F31 /* Licenses */ = { isa = PBXGroup; children = ( 17B68D4F25A4FED2005ED37C /* Sparkle-License.txt */, 17DFDE84251D309400A25F31 /* Hack-License.txt */, 17DFDE85251D309400A25F31 /* Lora-Cyrillic-OFL.txt */, 17DFDE86251D309400A25F31 /* OpenSans-License.txt */, ); path = Licenses; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 17DF328724C87D3500BCE2E3 /* WriteFreely-MultiPlatform (iOS) */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32B224C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (iOS)" */; buildPhases = ( 17DF328424C87D3500BCE2E3 /* Sources */, 17DF328524C87D3500BCE2E3 /* Frameworks */, 17DF328624C87D3500BCE2E3 /* Resources */, 17DF32C424C87E6700BCE2E3 /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = "WriteFreely-MultiPlatform (iOS)"; packageProductDependencies = ( 17DF32BF24C87D7B00BCE2E3 /* WriteFreely */, ); productName = "WriteFreely-MultiPlatform (iOS)"; productReference = 17DF328824C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */; productType = "com.apple.product-type.application"; }; 17DF328F24C87D3500BCE2E3 /* WriteFreely-MultiPlatform (macOS) */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32B524C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (macOS)" */; buildPhases = ( 17DF328C24C87D3500BCE2E3 /* Sources */, 17DF328D24C87D3500BCE2E3 /* Frameworks */, 17DF328E24C87D3500BCE2E3 /* Resources */, 17DF32C524C87FDB00BCE2E3 /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = "WriteFreely-MultiPlatform (macOS)"; packageProductDependencies = ( 17DF32C224C87D8D00BCE2E3 /* WriteFreely */, 17A4FED925924AF70037E96B /* Sparkle */, ); productName = "WriteFreely-MultiPlatform (macOS)"; productReference = 17DF329024C87D3500BCE2E3 /* WriteFreely for Mac.app */; productType = "com.apple.product-type.application"; }; 17DF329724C87D3500BCE2E3 /* Tests iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32B824C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests iOS" */; buildPhases = ( 17DF329424C87D3500BCE2E3 /* Sources */, 17DF329524C87D3500BCE2E3 /* Frameworks */, 17DF329624C87D3500BCE2E3 /* Resources */, ); buildRules = ( ); dependencies = ( 17DF329A24C87D3500BCE2E3 /* PBXTargetDependency */, ); name = "Tests iOS"; productName = "Tests iOS"; productReference = 17DF329824C87D3500BCE2E3 /* Tests iOS.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; 17DF32A224C87D3500BCE2E3 /* Tests macOS */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32BB24C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests macOS" */; buildPhases = ( 17DF329F24C87D3500BCE2E3 /* Sources */, 17DF32A024C87D3500BCE2E3 /* Frameworks */, 17DF32A124C87D3500BCE2E3 /* Resources */, ); buildRules = ( ); dependencies = ( 17DF32A524C87D3500BCE2E3 /* PBXTargetDependency */, ); name = "Tests macOS"; productName = "Tests macOS"; productReference = 17DF32A324C87D3500BCE2E3 /* Tests macOS.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 17DF327C24C87D3300BCE2E3 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1200; LastUpgradeCheck = 1200; TargetAttributes = { 17DF328724C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; }; 17DF328F24C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; }; 17DF329724C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; TestTargetID = 17DF328724C87D3500BCE2E3; }; 17DF32A224C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; TestTargetID = 17DF328F24C87D3500BCE2E3; }; }; }; buildConfigurationList = 17DF327F24C87D3300BCE2E3 /* Build configuration list for PBXProject "WriteFreely-MultiPlatform" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 17DF327B24C87D3300BCE2E3; packageReferences = ( 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */, 17A4FED825924AF70037E96B /* XCRemoteSwiftPackageReference "Sparkle" */, ); productRefGroup = 17DF328924C87D3500BCE2E3 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 17DF328724C87D3500BCE2E3 /* WriteFreely-MultiPlatform (iOS) */, 17DF328F24C87D3500BCE2E3 /* WriteFreely-MultiPlatform (macOS) */, 17DF329724C87D3500BCE2E3 /* Tests iOS */, 17DF32A224C87D3500BCE2E3 /* Tests macOS */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 17DF328624C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */, 17DFDE8B251D309400A25F31 /* OpenSans-License.txt in Resources */, 17DF32AE24C87D3500BCE2E3 /* Assets.xcassets in Resources */, 17D4F39E2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */, 17D4F36C2514EE2F00517CE6 /* LoraGX.ttf in Resources */, 17D4F3A52514F1E900517CE6 /* Hack-Regular.ttf in Resources */, 17DFDE89251D309400A25F31 /* Lora-Cyrillic-OFL.txt in Resources */, 17DFDE87251D309400A25F31 /* Hack-License.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF328E24C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF32AF24C87D3500BCE2E3 /* Assets.xcassets in Resources */, 17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */, 17B5103B2515448D00E9631F /* Credits.rtf in Resources */, 17D4F39F2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */, 17D4F3A62514F1E900517CE6 /* Hack-Regular.ttf in Resources */, 17D4F36D2514EE2F00517CE6 /* LoraGX.ttf in Resources */, 17DFDE8A251D309400A25F31 /* Lora-Cyrillic-OFL.txt in Resources */, 17DFDE88251D309400A25F31 /* Hack-License.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329624C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 17DF32A124C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 17DF32C424C87E6700BCE2E3 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Run SwiftLint on builds\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; 17DF32C524C87FDB00BCE2E3 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Run SwiftLint on builds\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 17DF328424C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF32AC24C87D3500BCE2E3 /* ContentView.swift in Sources */, 173E19D1254318F600440F0F /* RemoteChangePromptView.swift in Sources */, + 17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */, 17C42E622507D8E600072984 /* PostStatus.swift in Sources */, 1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */, 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */, 17C42E652509237800072984 /* PostListFilteredView.swift in Sources */, 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */, 17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */, + 17B37C4B25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */, 17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, 17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */, + 17B37C5D25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift in Sources */, 17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */, 17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */, 1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */, 17120DB224E1E19C002B9F6C /* SettingsHeaderView.swift in Sources */, 1756DBB724FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */, 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */, 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */, 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */, 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, 1756DC0124FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */, 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, 17120DA724E19D11002B9F6C /* SettingsView.swift in Sources */, 1756DC0324FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, 17120DA224E1985C002B9F6C /* AccountModel.swift in Sources */, 17120DA324E19A42002B9F6C /* PreferencesView.swift in Sources */, 1756AE6E24CB255B00FD7257 /* PostListModel.swift in Sources */, 173E19E3254329CC00440F0F /* PostTextEditingView.swift in Sources */, 174D313224EC2831006CA9EE /* WriteFreelyModel.swift in Sources */, 17C42E70250AA12300072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 17120DA124E19839002B9F6C /* AccountView.swift in Sources */, 1756AE7424CB26FA00FD7257 /* PostCellView.swift in Sources */, 17681E412519410E00D394AE /* UINavigationController+Appearance.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF328C24C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF32AD24C87D3500BCE2E3 /* ContentView.swift in Sources */, 1756DBBB24FED45500207AB8 /* LocalStorageManager.swift in Sources */, 17A4FEED25927E730037E96B /* AppDelegate.swift in Sources */, 174D313324EC2831006CA9EE /* WriteFreelyModel.swift in Sources */, 17D435E924E3128F0036B539 /* PreferencesModel.swift in Sources */, 17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */, 17479F152583D8E40072B7FB /* PostEditorSharingPicker.swift in Sources */, 17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, 17C42E662509237800072984 /* PostListFilteredView.swift in Sources */, 17120DAD24E1B99F002B9F6C /* AccountLoginView.swift in Sources */, 17466626256C0D0600629997 /* MacEditorTextView.swift in Sources */, 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */, 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */, 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */, 1756DC0424FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, 17B996DB2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 17BC618A25715318003363CA /* ActivePostToolbarView.swift in Sources */, 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */, 17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */, 17DF32AB24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, + 17B37C5725C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */, 17A5388C24DDC83F00DEFF9A /* AccountModel.swift in Sources */, 17B996D92502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, 1756DBB824FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */, + 17B37C4C25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */, 17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */, 1756AE6F24CB255B00FD7257 /* PostListModel.swift in Sources */, 1756DC0224FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */, 1756DBB424FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */, 17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */, 1780F6EF25895EDB00FE45FF /* PostCommands.swift in Sources */, + 17B37C5E25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift in Sources */, 170DFA35251BBC44001D82A0 /* PostEditorModel.swift in Sources */, 1756AE7524CB26FA00FD7257 /* PostCellView.swift in Sources */, 17A5388824DDA31F00DEFF9A /* MacAccountView.swift in Sources */, 17C42E632507D8E600072984 /* PostStatus.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329424C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF329D24C87D3500BCE2E3 /* Tests_iOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329F24C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF32A824C87D3500BCE2E3 /* Tests_macOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 17DF329A24C87D3500BCE2E3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 17DF328724C87D3500BCE2E3 /* WriteFreely-MultiPlatform (iOS) */; targetProxy = 17DF329924C87D3500BCE2E3 /* PBXContainerItemProxy */; }; 17DF32A524C87D3500BCE2E3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 17DF328F24C87D3500BCE2E3 /* WriteFreely-MultiPlatform (macOS) */; targetProxy = 17DF32A424C87D3500BCE2E3 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 17DF32B024C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 17DF32B124C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 17DF32B324C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 551; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 17DF32B424C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 551; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 17DF32B624C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 569; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = macOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 0.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely for Mac"; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; name = Debug; }; 17DF32B724C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 569; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = macOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 0.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely for Mac"; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; name = Release; }; 17DF32B924C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests iOS/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (iOS)"; }; name = Debug; }; 17DF32BA24C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests iOS/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (iOS)"; VALIDATE_PRODUCT = YES; }; name = Release; }; 17DF32BC24C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (macOS)"; }; name = Debug; }; 17DF32BD24C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (macOS)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 17DF327F24C87D3300BCE2E3 /* Build configuration list for PBXProject "WriteFreely-MultiPlatform" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B024C87D3500BCE2E3 /* Debug */, 17DF32B124C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32B224C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (iOS)" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B324C87D3500BCE2E3 /* Debug */, 17DF32B424C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32B524C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (macOS)" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B624C87D3500BCE2E3 /* Debug */, 17DF32B724C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32B824C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B924C87D3500BCE2E3 /* Debug */, 17DF32BA24C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32BB24C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32BC24C87D3500BCE2E3 /* Debug */, 17DF32BD24C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 17A4FED825924AF70037E96B /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/writefreely/Sparkle"; requirement = { branch = master; kind = branch; }; }; 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "git@github.com:writeas/writefreely-swift.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 0.3.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 17A4FED925924AF70037E96B /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = 17A4FED825924AF70037E96B /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; 17DF32BF24C87D7B00BCE2E3 /* WriteFreely */ = { isa = XCSwiftPackageProductDependency; package = 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */; productName = WriteFreely; }; 17DF32C224C87D8D00BCE2E3 /* WriteFreely */ = { isa = XCSwiftPackageProductDependency; package = 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */; productName = WriteFreely; }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( 1756DBB624FED3A400207AB8 /* LocalStorageModel.xcdatamodel */, ); currentVersion = 1756DBB624FED3A400207AB8 /* LocalStorageModel.xcdatamodel */; path = LocalStorageModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ }; rootObject = 17DF327C24C87D3300BCE2E3 /* Project object */; }