From 53ab32b7a33eb208a97123f279dd9a284a8f2058 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sun, 28 Jul 2024 06:02:02 -0400 Subject: [PATCH] Prep Mac app for release, fix bugs in iOS app (#258) * Update Sparkle to latest version * Bump minimum macOS target For launch, I propose we support the current version of macOS (14.x) and one version earlier (13.x). * Add WFNavigation wrapper to use NavigationSplitView in macOS * Replace NavigationView with WFNavigation in ContentView * Fix deprecation warnings on locale * Update docs for updating the Mac app * Fix for being sent back to post list on app reactivate * Bump build version * Remove debugging statements * Bump Sparkle version to address security fix --- Shared/Extensions/WriteFreelyModel+API.swift | 13 +- Shared/Navigation/ContentView.swift | 129 +++++++++++------- Shared/Navigation/WFNavigation.swift | 37 +++++ Shared/PostEditor/PostEditorModel.swift | 13 +- Shared/PostList/PostListView.swift | 14 +- Technotes/MacSoftwareUpdater.md | 29 ++-- .../project.pbxproj | 20 ++- 7 files changed, 165 insertions(+), 90 deletions(-) create mode 100644 Shared/Navigation/WFNavigation.swift diff --git a/Shared/Extensions/WriteFreelyModel+API.swift b/Shared/Extensions/WriteFreelyModel+API.swift index 3a9010b..1a574ab 100644 --- a/Shared/Extensions/WriteFreelyModel+API.swift +++ b/Shared/Extensions/WriteFreelyModel+API.swift @@ -94,9 +94,16 @@ extension WriteFreelyModel { } if post.language == nil { - if let languageCode = Locale.current.languageCode { - post.language = languageCode - post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft + if #available(iOS 16, macOS 13, *) { + if let languageCode = Locale.current.language.languageCode?.identifier { + post.language = languageCode + post.rtl = Locale.Language(identifier: languageCode).characterDirection == .rightToLeft + } + } else { + if let languageCode = Locale.current.languageCode { + post.language = languageCode + post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft + } } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index f67cda9..5b9d89a 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -5,62 +5,61 @@ struct ContentView: View { @EnvironmentObject var errorHandling: ErrorHandling var body: some View { - NavigationView { - #if os(macOS) - CollectionListView() - .withErrorHandling() - .toolbar { - Button( - action: { - NSApp.keyWindow?.contentViewController?.tryToPerform( - #selector(NSSplitViewController.toggleSidebar(_:)), with: nil - ) - }, - label: { Image(systemName: "sidebar.left") } - ) - .help("Toggle the sidebar's visibility.") - Spacer() - Button(action: { - withAnimation { - // Un-set the currently selected post - self.model.selectedPost = nil - } - // Create the new-post managed object - let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font) - withAnimation { - DispatchQueue.main.async { - // Load the new post in the editor - self.model.selectedPost = managedPost - } - } - }, label: { Image(systemName: "square.and.pencil") }) - .help("Create a new local draft.") - } - .frame(width: 200) - #else - CollectionListView() - .withErrorHandling() - #endif - - #if os(macOS) - ZStack { - PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) + #if os(macOS) + WFNavigation( + collectionList: { + CollectionListView() .withErrorHandling() - .frame(width: 300) - if model.isProcessingRequest { - ZStack { - Color(NSColor.controlBackgroundColor).opacity(0.75) - ProgressView() + .toolbar { + if #available(macOS 13, *) { + EmptyView() + } else { + Button( + action: { + NSApp.keyWindow?.contentViewController?.tryToPerform( + #selector(NSSplitViewController.toggleSidebar(_:)), with: nil + ) + }, + label: { Image(systemName: "sidebar.left") } + ) + .help("Toggle the sidebar's visibility.") + } + Spacer() + Button(action: { + withAnimation { + // Un-set the currently selected post + self.model.selectedPost = nil + } + // Create the new-post managed object + let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font) + withAnimation { + DispatchQueue.main.async { + // Load the new post in the editor + self.model.selectedPost = managedPost + } + } + }, label: { Image(systemName: "square.and.pencil") }) + .help("Create a new local draft.") + } + .frame(width: 200) + }, + postList: { + ZStack { + PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) + .withErrorHandling() + .frame(width: 300) + if model.isProcessingRequest { + ZStack { + Color(NSColor.controlBackgroundColor).opacity(0.75) + ProgressView() + } } } + }, + postDetail: { + NoSelectedPostView(isConnected: $model.hasNetworkConnection) } - #else - PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) - .withErrorHandling() - #endif - - NoSelectedPostView(isConnected: $model.hasNetworkConnection) - } + ) .environmentObject(model) .onChange(of: model.hasError) { value in if value { @@ -72,6 +71,32 @@ struct ContentView: View { model.hasError = false } } + #else + WFNavigation( + collectionList: { + CollectionListView() + .withErrorHandling() + }, + postList: { + PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts) + .withErrorHandling() + }, + postDetail: { + NoSelectedPostView(isConnected: $model.hasNetworkConnection) + } + ) + .environmentObject(model) + .onChange(of: model.hasError) { value in + if value { + if let error = model.currentError { + self.errorHandling.handle(error: error) + } else { + self.errorHandling.handle(error: AppError.genericError()) + } + model.hasError = false + } + } + #endif } } diff --git a/Shared/Navigation/WFNavigation.swift b/Shared/Navigation/WFNavigation.swift new file mode 100644 index 0000000..b5b0d56 --- /dev/null +++ b/Shared/Navigation/WFNavigation.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct WFNavigation: View + where CollectionList: View, PostList: View, PostDetail: View { + + private var collectionList: CollectionList + private var postList: PostList + private var postDetail: PostDetail + + init( + @ViewBuilder collectionList: () -> CollectionList, + @ViewBuilder postList: () -> PostList, + @ViewBuilder postDetail: () -> PostDetail + ) { + self.collectionList = collectionList() + self.postList = postList() + self.postDetail = postDetail() + } + + var body: some View { + #if os(macOS) + NavigationSplitView { + collectionList + } content: { + postList + } detail: { + postDetail + } + #else + NavigationView { + collectionList + postList + postDetail + } + #endif + } +} diff --git a/Shared/PostEditor/PostEditorModel.swift b/Shared/PostEditor/PostEditorModel.swift index 319beaa..c6fda4c 100644 --- a/Shared/PostEditor/PostEditorModel.swift +++ b/Shared/PostEditor/PostEditorModel.swift @@ -48,9 +48,16 @@ struct PostEditorModel { default: managedPost.appearance = "serif" } - if let languageCode = Locale.current.languageCode { - managedPost.language = languageCode - managedPost.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft + if #available(iOS 16, macOS 13, *) { + if let languageCode = Locale.current.language.languageCode?.identifier { + managedPost.language = languageCode + managedPost.rtl = Locale.Language(identifier: languageCode).characterDirection == .rightToLeft + } + } else { + if let languageCode = Locale.current.languageCode { + managedPost.language = languageCode + managedPost.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft + } } return managedPost } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 89fe9fa..06b4714 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -126,18 +126,18 @@ struct PostListView: View { .frame(height: frameHeight) .background(Color(UIColor.systemGray5)) .overlay(Divider(), alignment: .top) - .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in - // We use this to invalidate and refresh the view, so that new posts created outside of the app (e.g., - // in the action extension) show up. - withAnimation { - self.filteredListViewId += 1 - } - } } .ignoresSafeArea(.all, edges: .bottom) .onAppear { + // Set the selected collection and whether or not we want to show all posts model.selectedCollection = selectedCollection model.showAllPosts = showAllPosts + + // We use this to invalidate and refresh the view, so that new posts created outside of the app (e.g., + // in the action extension) show up. + withAnimation { + self.filteredListViewId += 1 + } } .onChange(of: model.hasError) { value in if value { diff --git a/Technotes/MacSoftwareUpdater.md b/Technotes/MacSoftwareUpdater.md index 690f3b8..319ec0c 100644 --- a/Technotes/MacSoftwareUpdater.md +++ b/Technotes/MacSoftwareUpdater.md @@ -2,13 +2,6 @@ To make updating the Mac app easy, we're using the [Sparkle framework][1]. -This is added to the project via the Swift Package Manager (SPM), but at the time of writing, tagged versions of Sparkle do not yet support -SPM — the dependency can only be added from a branch or commit. To avoid any surprises arising from updates to the project's `master` -branch, we're using [WriteFreely's fork of Sparkle][2]. Updates to the forked repository from upstream should be considered dangerous and -tested thoroughly before merging into `main`. - -WriteFreely for Mac uses the v1.x branch of Sparkle, and is therefore not a sandboxed app. - ## Troubleshooting ### If Xcode throws an error when you try to build the project @@ -22,21 +15,22 @@ You should then be able to build and run the Mac target. ### If you can't run `generate_keys` because "Apple cannot check it for malicious software" -There may be a code signing issue with Sparkle. Right-click on `generate_keys` in the Finder and choose Open ([reference][3]). +If you run into a code signing issue with Sparkle, right-click on `generate_keys` in the Finder and choose Open ([reference][2]). ## Deploying Updates -To [publish an update to the app][5], you'll need the **Sparkle-for-Swift-Package-Manager.zip** [archive][4] — specifically, you'll need the -`generate_appcast` tool. Download and de-compress the archive. +To [publish an update to the app][4], you'll need the **Sparkle-for-Swift-Package-Manager.zip** [archive][3] —  +specifically, you'll need the `generate_appcast` tool. Download and de-compress the archive. -You will need some credentials and signing certificates to proceed with this process; speak to the project maintainer if you're responsible for -creating the update, and confirm you have: +You will need some credentials and signing certificates to proceed with this process; speak to the project maintainer if +you're responsible for creating the update, and confirm you have: - the app's Developer ID Application certificate (check your Mac's system Keychain) - the Sparkle EdDSA signing key (again, check your Mac's system Keychain) -Sign and notarize the app archive, then click on **Export Notarized App** in Xcode's Organizer window. Open the Terminal and navigate to -where you de-compressed the Sparkle-for-Swift-Package-Manager archive, then create a zip file that preserves symlinks: +Sign and notarize the app archive, then click on **Export Notarized App** in Xcode's Organizer window. Open the Terminal +and navigate to where you de-compressed the Sparkle-for-Swift-Package-Manager archive, then create a zip file that +preserves symlinks: ```bash % ditto -c -k --sequesterRsrc --keepParent @@ -60,7 +54,6 @@ and they'll be made available to users. [1]: https://sparkle-project.org -[2]: https://github.com/writefreely/Sparkle -[3]: https://github.com/sparkle-project/Sparkle/issues/1701#issuecomment-752249920 -[4]: https://github.com/sparkle-project/Sparkle/releases/tag/1.24.0 -[5]: https://sparkle-project.org/documentation/publishing/ +[2]: https://github.com/sparkle-project/Sparkle/issues/1701#issuecomment-752249920 +[3]: https://github.com/sparkle-project/Sparkle/releases/tag/1.24.0 +[4]: https://sparkle-project.org/documentation/publishing/ diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 5abb91f..a1d5f0c 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -139,6 +139,8 @@ 37095AE02AA4A0E700C9C5F8 /* NoSelectedPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */; }; 37113EF92A98C10A00B36B98 /* NoSelectedPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */; }; 375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */; }; + 376A350D2B5D5C8E00255D61 /* WFNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A350C2B5D5C8E00255D61 /* WFNavigation.swift */; }; + 376A350E2B5D5C8E00255D61 /* WFNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A350C2B5D5C8E00255D61 /* WFNavigation.swift */; }; 3779389729EC0C880032D6C1 /* HelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3779389629EC0C880032D6C1 /* HelpCommands.swift */; }; 37F749D129B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */; }; 37F749D229B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */; }; @@ -274,6 +276,7 @@ 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = ""; }; 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSelectedPostView.swift; sourceTree = ""; }; 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = ""; }; + 376A350C2B5D5C8E00255D61 /* WFNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WFNavigation.swift; sourceTree = ""; }; 3779389629EC0C880032D6C1 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = ""; }; 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePostListFilteredView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -590,6 +593,7 @@ children = ( 17DF328224C87D3300BCE2E3 /* ContentView.swift */, 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */, + 376A350C2B5D5C8E00255D61 /* WFNavigation.swift */, ); path = Navigation; sourceTree = ""; @@ -947,6 +951,7 @@ 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */, 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 37F749D129B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */, + 376A350D2B5D5C8E00255D61 /* WFNavigation.swift in Sources */, 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */, 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */, 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, @@ -997,6 +1002,7 @@ 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */, 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */, 1756DC0424FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, + 376A350E2B5D5C8E00255D61 /* WFNavigation.swift in Sources */, 17B996DB2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 17BC618A25715318003363CA /* ActivePostToolbarView.swift in Sources */, 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */, @@ -1240,7 +1246,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 708; + CURRENT_PROJECT_VERSION = 710; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -1249,7 +1255,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.17; + MARKETING_VERSION = 1.0.18; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; @@ -1266,7 +1272,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 708; + CURRENT_PROJECT_VERSION = 710; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -1275,7 +1281,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.17; + MARKETING_VERSION = 1.0.18; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; @@ -1303,7 +1309,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely for Mac"; @@ -1330,7 +1336,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely for Mac"; @@ -1503,7 +1509,7 @@ repositoryURL = "https://github.com/sparkle-project/Sparkle"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 2.3.0; + minimumVersion = 2.6.2; }; }; /* End XCRemoteSwiftPackageReference section */