Page MenuHomeMusing Studio

No OneTemporary

diff --git a/Shared/Account/AccountLoginView.swift b/Shared/Account/AccountLoginView.swift
index d90d7f7..75237c9 100644
--- a/Shared/Account/AccountLoginView.swift
+++ b/Shared/Account/AccountLoginView.swift
@@ -1,106 +1,104 @@
import SwiftUI
struct AccountLoginView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@State private var alertMessage: String = ""
@State private var username: String = ""
@State private var password: String = ""
@State private var server: String = ""
var body: some View {
VStack {
Text("Log in to publish and share your posts.")
.font(.caption)
.foregroundColor(.secondary)
HStack {
Image(systemName: "person.circle")
.foregroundColor(.gray)
#if os(iOS)
TextField("Username", text: $username)
.autocapitalization(.none)
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
#else
TextField("Username", text: $username)
#endif
}
HStack {
Image(systemName: "lock.circle")
.foregroundColor(.gray)
#if os(iOS)
SecureField("Password", text: $password)
.autocapitalization(.none)
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
#else
SecureField("Password", text: $password)
#endif
}
HStack {
Image(systemName: "link.circle")
.foregroundColor(.gray)
#if os(iOS)
TextField("Server URL", text: $server)
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
#else
TextField("Server URL", text: $server)
#endif
}
Spacer()
if model.isLoggingIn {
ProgressView("Logging in...")
.padding()
} else {
Button(action: {
#if os(iOS)
hideKeyboard()
#endif
// If the server string is not prefixed with a scheme, prepend "https://" to it.
if !(server.hasPrefix("https://") || server.hasPrefix("http://")) {
server = "https://\(server)"
}
// We only need the protocol and host from the URL, so drop anything else.
let url = URLComponents(string: server)
if let validURL = url {
let scheme = validURL.scheme
let host = validURL.host
var hostURL = URLComponents()
hostURL.scheme = scheme
hostURL.host = host
server = hostURL.string ?? server
model.login(
to: URL(string: server)!,
as: username, password: password
)
} else {
self.errorHandling.handle(error: AccountError.invalidServerURL)
}
}, label: {
Text("Log In")
})
.disabled(
model.account.isLoggedIn || (username.isEmpty || password.isEmpty || server.isEmpty)
)
.padding()
}
}
- .alert(isPresented: $model.isPresentingLoginErrorAlert) {
- Alert(
- title: Text("Error Logging In"),
- message: Text(model.loginErrorMessage ?? "An unknown error occurred while trying to login."),
- dismissButton: .default(Text("OK"))
- )
+ .onChange(of: model.shouldHandleError) { _ in
+ guard let error = model.currentError else { return }
+ self.errorHandling.handle(error: error)
+ model.currentError = nil
}
}
}
struct AccountLoginView_Previews: PreviewProvider {
static var previews: some View {
AccountLoginView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift
index 5f087a8..a0dcd85 100644
--- a/Shared/Account/AccountLogoutView.swift
+++ b/Shared/Account/AccountLogoutView.swift
@@ -1,80 +1,81 @@
import SwiftUI
struct AccountLogoutView: View {
@EnvironmentObject var model: WriteFreelyModel
+ @EnvironmentObject var errorHandling: ErrorHandling
@State private var isPresentingLogoutConfirmation: Bool = false
@State private var editedPostsWarningString: String = ""
var body: some View {
#if os(iOS)
VStack {
Spacer()
VStack {
Text("Logged in as \(model.account.username)")
Text("on \(model.account.server)")
}
Spacer()
Button(action: logoutHandler, label: {
Text("Log Out")
})
}
.actionSheet(isPresented: $isPresentingLogoutConfirmation, content: {
ActionSheet(
title: Text("Log Out?"),
message: Text("\(editedPostsWarningString)You won't lose any local posts. Are you sure?"),
buttons: [
.destructive(Text("Log Out"), action: {
model.logout()
}),
.cancel()
]
)
})
#else
VStack {
Spacer()
VStack {
Text("Logged in as \(model.account.username)")
Text("on \(model.account.server)")
}
Spacer()
Button(action: logoutHandler, label: {
Text("Log Out")
})
}
.alert(isPresented: $isPresentingLogoutConfirmation) {
Alert(
title: Text("Log Out?"),
message: Text("\(editedPostsWarningString)You won't lose any local posts. Are you sure?"),
primaryButton: .cancel(Text("Cancel"), action: { self.isPresentingLogoutConfirmation = false }),
secondaryButton: .destructive(Text("Log Out"), action: model.logout )
)
}
#endif
}
func logoutHandler() {
let request = WFAPost.createFetchRequest()
request.predicate = NSPredicate(format: "status == %i", 1)
do {
let editedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request)
if editedPosts.count == 1 {
editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited post. "
}
if editedPosts.count > 1 {
editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited posts. "
}
} catch {
- fatalError("Error: failed to fetch cached posts")
+ self.errorHandling.handle(error: LocalStoreError.couldNotFetchPosts("edited"))
}
self.isPresentingLogoutConfirmation = true
}
}
struct AccountLogoutView_Previews: PreviewProvider {
static var previews: some View {
AccountLogoutView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift
index 9241026..d4b33fb 100644
--- a/Shared/Account/AccountView.swift
+++ b/Shared/Account/AccountView.swift
@@ -1,27 +1,28 @@
import SwiftUI
struct AccountView: View {
@EnvironmentObject var model: WriteFreelyModel
var body: some View {
if model.account.isLoggedIn {
HStack {
Spacer()
AccountLogoutView()
+ .withErrorHandling()
Spacer()
}
.padding()
} else {
AccountLoginView()
.withErrorHandling()
.padding(.top)
}
}
}
struct AccountLogin_Previews: PreviewProvider {
static var previews: some View {
AccountView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/ErrorHandling/ErrorConstants.swift b/Shared/ErrorHandling/ErrorConstants.swift
index 1873335..467b858 100644
--- a/Shared/ErrorHandling/ErrorConstants.swift
+++ b/Shared/ErrorHandling/ErrorConstants.swift
@@ -1,73 +1,116 @@
import Foundation
// MARK: - Network Errors
enum NetworkError: Error {
case noConnectionError
}
extension NetworkError: LocalizedError {
public var errorDescription: String? {
switch self {
case .noConnectionError:
return NSLocalizedString(
"There is no internet connection at the moment. Please reconnect or try again later.",
comment: ""
)
}
}
}
+// MARK: - Keychain Errors
+
+enum KeychainError: Error {
+ case couldNotStoreAccessToken
+ case couldNotPurgeAccessToken
+}
+
+extension KeychainError: LocalizedError {
+ public var errorDescription: String? {
+ switch self {
+ case .couldNotStoreAccessToken:
+ return NSLocalizedString("There was a problem storing your access token in the Keychain.", comment: "")
+ case .couldNotPurgeAccessToken:
+ return NSLocalizedString("Something went wrong purging the token from the Keychain.", comment: "")
+ }
+ }
+}
+
// MARK: - Account Errors
enum AccountError: Error {
case invalidPassword
case usernameNotFound
case serverNotFound
case invalidServerURL
case couldNotSaveTokenToKeychain
case couldNotFetchTokenFromKeychain
case couldNotDeleteTokenFromKeychain
+ case unknownLoginError
+ case genericAuthError
}
extension AccountError: LocalizedError {
public var errorDescription: String? {
switch self {
case .serverNotFound:
return NSLocalizedString(
"The server could not be found. Please check the information you've entered and try again.",
comment: ""
)
case .invalidPassword:
return NSLocalizedString(
"Invalid password. Please check that you've entered your password correctly and try logging in again.",
comment: ""
)
case .usernameNotFound:
return NSLocalizedString(
"Username not found. Did you use your email address by mistake?",
comment: ""
)
case .invalidServerURL:
return NSLocalizedString(
"Please enter a valid instance domain name. It should look like \"https://example.com\" or \"write.as\".", // swiftlint:disable:this line_length
comment: ""
)
case .couldNotSaveTokenToKeychain:
return NSLocalizedString(
"There was a problem trying to save your access token to the device, please try logging in again.",
comment: ""
)
case .couldNotFetchTokenFromKeychain:
return NSLocalizedString(
"There was a problem trying to fetch your access token from the device, please try logging in again.",
comment: ""
)
case .couldNotDeleteTokenFromKeychain:
return NSLocalizedString(
"There was a problem trying to delete your access token from the device, please try logging out again.",
comment: ""
)
+ case .genericAuthError:
+ return NSLocalizedString("Something went wrong, please try logging in again.", comment: "")
+ case .unknownLoginError:
+ return NSLocalizedString("An unknown error occurred while trying to login", comment: "")
+ }
+ }
+}
+
+// MARK: - Local Store Errors
+
+enum LocalStoreError: Error {
+ case couldNotFetchPosts(String)
+}
+
+extension LocalStoreError: LocalizedError {
+ public var errorDescription: String? {
+ switch self {
+ case .couldNotFetchPosts(let postFilter):
+ if postFilter.isEmpty {
+ return NSLocalizedString("Failed to fetch posts from local store", comment: "")
+ } else {
+ return NSLocalizedString("Failed to fetch \(postFilter) posts from local store", comment: "")
+ }
}
}
}
diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
index 8b1e5d0..d438621 100644
--- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
+++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
@@ -1,284 +1,277 @@
import Foundation
import WriteFreely
extension WriteFreelyModel {
func loginHandler(result: Result<WFUser, Error>) {
DispatchQueue.main.async {
self.isLoggingIn = false
}
do {
let user = try result.get()
fetchUserCollections()
fetchUserPosts()
do {
try saveTokenToKeychain(user.token, username: user.username, server: account.server)
DispatchQueue.main.async {
self.account.login(user)
}
} catch {
DispatchQueue.main.async {
- self.loginErrorMessage = "There was a problem storing your access token to the Keychain."
- self.isPresentingLoginErrorAlert = true
+ self.currentError = KeychainError.couldNotStoreAccessToken
}
}
} catch WFError.notFound {
DispatchQueue.main.async {
- self.loginErrorMessage = AccountError.usernameNotFound.localizedDescription
- self.isPresentingLoginErrorAlert = true
+ self.currentError = AccountError.usernameNotFound
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
- self.loginErrorMessage = AccountError.invalidPassword.localizedDescription
- self.isPresentingLoginErrorAlert = true
+ self.currentError = AccountError.invalidPassword
}
} catch {
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == -1003 {
DispatchQueue.main.async {
- self.loginErrorMessage = AccountError.serverNotFound.localizedDescription
- self.isPresentingLoginErrorAlert = true
+ self.currentError = AccountError.serverNotFound
}
} else {
DispatchQueue.main.async {
- self.loginErrorMessage = error.localizedDescription
- self.isPresentingLoginErrorAlert = true
+ self.currentError = error
}
}
}
}
func logoutHandler(result: Result<Bool, Error>) {
do {
_ = try result.get()
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
LocalStorageManager.standard.purgeUserCollections()
self.posts.purgePublishedPosts()
}
} catch {
- fatalError("Something went wrong purging the token from the Keychain.")
+ self.currentError = KeychainError.couldNotPurgeAccessToken
}
} 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.standard.purgeUserCollections()
self.posts.purgePublishedPosts()
}
} catch {
- fatalError("Something went wrong purging the token from the Keychain.")
+ self.currentError = KeychainError.couldNotPurgeAccessToken
}
} 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.standard.container.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.standard.saveContext()
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
- self.loginErrorMessage = "Something went wrong, please try logging in again."
- self.isPresentingLoginErrorAlert = true
+ self.currentError = AccountError.genericAuthError
}
self.logout()
} catch {
fatalError(error.localizedDescription)
}
}
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.standard.container.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 {
fatalError("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.standard.container.viewContext)
self.importData(from: fetchedPost, into: managedPost)
managedPost.collectionAlias = fetchedPost.collectionAlias
managedPost.wasDeletedFromServer = false
}
}
}
DispatchQueue.main.async {
for post in postsToDelete { post.wasDeletedFromServer = true }
LocalStorageManager.standard.saveContext()
}
} catch {
fatalError(error.localizedDescription)
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
- self.loginErrorMessage = "Something went wrong, please try logging in again."
- self.isPresentingLoginErrorAlert = true
+ self.currentError = AccountError.genericAuthError
}
self.logout()
} catch {
fatalError("Error: Failed to fetch cached posts")
}
}
func publishHandler(result: Result<WFPost, Error>) {
// 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 {
importData(from: fetchedPost, into: updatingPost)
DispatchQueue.main.async {
LocalStorageManager.standard.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.standard.container.viewContext.fetch(request)
guard let cachedPost = cachedPostsResults.first else {
fatalError("Could not get cached post from results")
}
importData(from: fetchedPost, into: cachedPost)
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch {
fatalError("Error: Failed to fetch cached posts")
}
}
} catch {
fatalError(error.localizedDescription)
}
}
func updateFromServerHandler(result: Result<WFPost, Error>) {
// 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 {
fatalError("Could not get cached post")
}
importData(from: fetchedPost, into: cachedPost)
cachedPost.hasNewerRemoteCopy = false
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch {
fatalError(error.localizedDescription)
}
}
func movePostHandler(result: Result<Bool, Error>) {
// 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 {
fatalError("Could not update post from server")
}
}
} catch {
DispatchQueue.main.async {
LocalStorageManager.standard.container.viewContext.rollback()
}
fatalError(error.localizedDescription)
}
}
private func importData(from fetchedPost: WFPost, into cachedPost: WFAPost) {
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
}
}
diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index ecb575f..c9487ab 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,83 +1,85 @@
import Foundation
import WriteFreely
import Security
import Network
// MARK: - WriteFreelyModel
final 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 shouldHandleError: Bool = false
@Published var isPresentingNetworkErrorAlert: Bool = false
@Published var postToDelete: WFAPost?
#if os(iOS)
@Published var isPresentingSettingsView: Bool = false
#endif
static var shared = WriteFreelyModel()
- var loginErrorMessage: String?
+ var currentError: Error? {
+ didSet {
+ self.shouldHandleError = currentError != nil
+ }
+ }
// 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
internal var client: WFClient?
private let defaults = UserDefaults.shared
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
internal var postToUpdate: WFAPost?
init() {
DispatchQueue.main.async {
self.preferences.appearance = self.defaults.integer(forKey: WFDefaults.colorSchemeIntegerKey)
self.preferences.font = self.defaults.integer(forKey: WFDefaults.defaultFontIntegerKey)
self.account.restoreState()
if self.account.isLoggedIn {
guard let serverURL = URL(string: self.account.server) else {
print("Server URL not found")
return
}
do {
guard let token = try self.fetchTokenFromKeychain(
username: self.account.username,
server: self.account.server
) else {
- self.loginErrorMessage = AccountError.couldNotFetchTokenFromKeychain.localizedDescription
- self.isPresentingLoginErrorAlert = true
+ self.currentError = AccountError.couldNotFetchTokenFromKeychain
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()
} catch {
- self.loginErrorMessage = AccountError.couldNotFetchTokenFromKeychain.localizedDescription
- self.isPresentingLoginErrorAlert = true
+ self.currentError = AccountError.couldNotFetchTokenFromKeychain
}
}
}
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
self.hasNetworkConnection = path.status == .satisfied
}
}
monitor.start(queue: queue)
}
}
diff --git a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist
index 33a3444..155f2da 100644
--- a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -1,24 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>ActionExtension-iOS.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>1</integer>
+ <integer>0</integer>
</dict>
<key>WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>2</integer>
+ <integer>1</integer>
</dict>
<key>WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>0</integer>
+ <integer>2</integer>
</dict>
</dict>
</dict>
</plist>

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jan 31, 9:37 AM (1 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3145628

Event Timeline