From e4c22a0205f9482684113cea7122d283b9e66ba9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 19 Oct 2023 22:50:20 -0400 Subject: [PATCH 01/15] Compile for visionOS --- OpenInTusker/ActionViewController.swift | 9 +-- .../Controllers/AttachmentRowController.swift | 9 +++ .../AttachmentThumbnailController.swift | 8 +++ .../Controllers/ComposeController.swift | 8 +++ .../Controllers/PollController.swift | 6 ++ .../Sources/ComposeUI/KeyboardReader.swift | 4 ++ .../Sources/ComposeUI/PKDrawing+Render.swift | 2 +- .../ComposeUI/View+ForwardsCompat.swift | 6 ++ .../ComposeUI/Views/LanguagePicker.swift | 24 ++++++-- .../DuckableContainerViewController.swift | 2 + ShareExtension/ShareHostingController.swift | 4 ++ Tusker.xcodeproj/project.pbxproj | 44 +++++++------- Tusker/API/FavoriteService.swift | 4 ++ Tusker/API/MastodonController.swift | 6 ++ Tusker/API/ReblogService.swift | 4 ++ Tusker/AppDelegate.swift | 15 ++++- Tusker/Caching/ImageCache.swift | 8 ++- .../MastodonCachePersistentStore.swift | 4 ++ Tusker/CoreData/TimelinePosition.swift | 1 + Tusker/Extensions/PKDrawing+Render.swift | 33 ----------- Tusker/MultiThreadDictionary.swift | 15 +++++ Tusker/Preferences/Colors.swift | 14 ++++- Tusker/Scenes/MainSceneDelegate.swift | 6 ++ Tusker/Scenes/TuskerSceneDelegate.swift | 6 ++ .../GalleryViewController.swift | 2 + .../Compose/ComposeHostingController.swift | 38 ++++++------ .../AddHashtagPinnedTimelineView.swift | 2 + .../Customize Timelines/EditFilterView.swift | 11 ++++ .../PinnedTimelinesView.swift | 13 ++++ .../FeaturedProfileCollectionViewCell.swift | 3 + ...ggestedProfileCardCollectionViewCell.swift | 5 ++ .../TrendingLinkCardCollectionViewCell.swift | 3 + .../Explore/TrendingLinksViewController.swift | 2 + .../Explore/TrendsViewController.swift | 5 +- .../FastAccountSwitcherViewController.swift | 14 +++++ .../Large Image/LargeImageContentView.swift | 5 ++ .../LargeImageViewController.swift | 2 + .../LoadingLargeImageViewController.swift | 2 + ...ountSwitchingContainerViewController.swift | 6 ++ Tusker/Screens/Main/Duckable+Root.swift | 4 ++ .../Main/MainTabBarViewController.swift | 25 +++----- ...equestNotificationCollectionViewCell.swift | 4 ++ ...otificationsCollectionViewController.swift | 4 ++ .../InstanceSelectorTableViewController.swift | 2 + .../Preferences/AppearancePrefsView.swift | 3 +- .../OppositeCollapseKeywordsView.swift | 3 + .../Preferences/Tip Jar/TipJarView.swift | 8 +++ .../Screens/Report/ReportAddStatusView.swift | 6 ++ .../Report/ReportSelectRulesView.swift | 1 + Tusker/Screens/Report/ReportView.swift | 2 + .../Search/SearchResultsViewController.swift | 2 + .../Timeline/TimelineViewController.swift | 16 +++++ .../Utilities/CustomAlertController.swift | 8 +++ .../EnhancedNavigationViewController.swift | 6 ++ .../Utilities/InteractivePushTransition.swift | 4 ++ .../UserActivityHandlingContext.swift | 4 ++ Tusker/TuskerNavigationDelegate.swift | 13 +++- Tusker/Views/Attachments/AttachmentView.swift | 8 +++ .../Attachments/GifvAttachmentView.swift | 4 ++ Tusker/Views/BaseEmojiLabel.swift | 11 +++- Tusker/Views/BasicTableViewCell.xib | 30 ---------- Tusker/Views/ContentTextView.swift | 2 + Tusker/Views/Poll/PollOptionsView.swift | 6 ++ Tusker/Views/Poll/StatusPollView.swift | 2 + .../ProfileFieldValueView.swift | 4 ++ .../Profile Header/ProfileFieldsView.swift | 10 ++++ .../Profile Header/ProfileHeaderView.swift | 2 + Tusker/Views/ScrollingSegmentedControl.swift | 16 +++++ Tusker/Views/Status/StatusCardView.swift | 5 ++ .../Status/StatusMetaIndicatorsView.swift | 13 ++++ Tusker/Views/Toast/ToastConfiguration.swift | 59 +++++++++++-------- Tusker/Views/TrendHistoryView.swift | 3 + 72 files changed, 480 insertions(+), 165 deletions(-) delete mode 100644 Tusker/Extensions/PKDrawing+Render.swift delete mode 100644 Tusker/Views/BasicTableViewCell.xib diff --git a/OpenInTusker/ActionViewController.swift b/OpenInTusker/ActionViewController.swift index ff89ef27..20a94686 100644 --- a/OpenInTusker/ActionViewController.swift +++ b/OpenInTusker/ActionViewController.swift @@ -8,6 +8,7 @@ import UIKit import MobileCoreServices +import UniformTypeIdentifiers class ActionViewController: UIViewController { @@ -32,10 +33,10 @@ class ActionViewController: UIViewController { private func findURLFromWebPage(completion: @escaping (URLComponents?) -> Void) { for item in extensionContext!.inputItems as! [NSExtensionItem] { for provider in item.attachments! { - guard provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) else { + guard provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else { continue } - provider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil) { (result, error) in + provider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil) { (result, error) in guard let result = result as? [String: Any], let jsResult = result[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any], let urlString = jsResult["activityPubURL"] as? String ?? jsResult["url"] as? String, @@ -56,10 +57,10 @@ class ActionViewController: UIViewController { private func findURLItem(completion: @escaping (URLComponents?) -> Void) { for item in extensionContext!.inputItems as! [NSExtensionItem] { for provider in item.attachments! { - guard provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) else { + guard provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else { continue } - provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (result, error) in + provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { (result, error) in guard let result = result as? URL, let components = URLComponents(url: result, resolvingAgainstBaseURL: false) else { completion(nil) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift index 0762c2d3..16273c85 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift @@ -177,11 +177,19 @@ class AttachmentRowController: ViewController { Text(error.localizedDescription) } .onAppear(perform: controller.updateAttachmentDescriptionState) + #if os(visionOS) + .onChange(of: textEditorFocused) { + if !textEditorFocused && controller.focusAttachmentOnTextEditorUnfocus { + controller.focusAttachment() + } + } + #else .onChange(of: textEditorFocused) { newValue in if !newValue && controller.focusAttachmentOnTextEditorUnfocus { controller.focusAttachment() } } + #endif } @ViewBuilder @@ -208,6 +216,7 @@ extension AttachmentRowController { private extension View { @available(iOS, obsoleted: 16.0) + @available(visionOS 1.0, *) @ViewBuilder func contextMenu(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View { if #available(iOS 16.0, *) { diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentThumbnailController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentThumbnailController.swift index e2559c1f..8bc31e2c 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentThumbnailController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentThumbnailController.swift @@ -40,9 +40,13 @@ class AttachmentThumbnailController: ViewController { case .video, .gifv: let asset = AVURLAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) + #if os(visionOS) + #warning("Use async AVAssetImageGenerator.image(at:)") + #else if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) { self.image = UIImage(cgImage: cgImage) } + #endif case .audio, .unknown: break @@ -87,9 +91,13 @@ class AttachmentThumbnailController: ViewController { if type.conforms(to: .movie) { let asset = AVURLAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) + #if os(visionOS) + #warning("Use async AVAssetImageGenerator.image(at:)") + #else if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) { self.image = UIImage(cgImage: cgImage) } + #endif } else if let data = try? Data(contentsOf: url) { if type == .gif { self.gifController = GIFController(gifData: data) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift index 93682956..369d89bd 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift @@ -270,7 +270,9 @@ public final class ComposeController: ViewController { @OptionalObservedObject var poster: PostService? @EnvironmentObject var controller: ComposeController @EnvironmentObject var draft: Draft + #if !os(visionOS) @StateObject private var keyboardReader = KeyboardReader() + #endif @State private var globalFrameOutsideList = CGRect.zero init(poster: PostService?) { @@ -315,8 +317,10 @@ public final class ComposeController: ViewController { ControllerView(controller: { controller.toolbarController }) } + #if !os(visionOS) // on iPadOS15, the toolbar ends up below the keyboard's toolbar without this .padding(.bottom, keyboardInset) + #endif .transition(.move(edge: .bottom)) } } @@ -414,7 +418,9 @@ public final class ComposeController: ViewController { .listRowBackground(config.backgroundColor) } .listStyle(.plain) + #if !os(visionOS) .scrollDismissesKeyboardInteractivelyIfAvailable() + #endif .disabled(controller.isPosting) } @@ -457,6 +463,7 @@ public final class ComposeController: ViewController { } } + #if !os(visionOS) @available(iOS, obsoleted: 16.0) private var keyboardInset: CGFloat { if #unavailable(iOS 16.0), @@ -467,6 +474,7 @@ public final class ComposeController: ViewController { return 0 } } + #endif } } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/PollController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/PollController.swift index 712bc42b..3914e51b 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/PollController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/PollController.swift @@ -123,9 +123,15 @@ class PollController: ViewController { RoundedRectangle(cornerRadius: 10, style: .continuous) .foregroundColor(backgroundColor) ) + #if os(visionOS) + .onChange(of: controller.duration) { + poll.duration = controller.duration.timeInterval + } + #else .onChange(of: controller.duration) { newValue in poll.duration = newValue.timeInterval } + #endif } private var backgroundColor: Color { diff --git a/Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift b/Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift index 8ecbc6e4..11aa3771 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/KeyboardReader.swift @@ -5,6 +5,8 @@ // Created by Shadowfacts on 3/7/23. // +#if !os(visionOS) + import UIKit import Combine @@ -37,3 +39,5 @@ class KeyboardReader: ObservableObject { } } } + +#endif diff --git a/Packages/ComposeUI/Sources/ComposeUI/PKDrawing+Render.swift b/Packages/ComposeUI/Sources/ComposeUI/PKDrawing+Render.swift index f6eb4273..366f9510 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/PKDrawing+Render.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/PKDrawing+Render.swift @@ -11,7 +11,7 @@ import PencilKit extension PKDrawing { - func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage { + func imageInLightMode(from rect: CGRect, scale: CGFloat = 1) -> UIImage { let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light) var drawingImage: UIImage! lightTraitCollection.performAsCurrent { diff --git a/Packages/ComposeUI/Sources/ComposeUI/View+ForwardsCompat.swift b/Packages/ComposeUI/Sources/ComposeUI/View+ForwardsCompat.swift index 418e36b9..7b1dc3c3 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/View+ForwardsCompat.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/View+ForwardsCompat.swift @@ -8,6 +8,11 @@ import SwiftUI extension View { + #if os(visionOS) + func scrollDisabledIfAvailable(_ disabled: Bool) -> some View { + self.scrollDisabled(disabled) + } + #else @available(iOS, obsoleted: 16.0) @ViewBuilder func scrollDisabledIfAvailable(_ disabled: Bool) -> some View { @@ -17,4 +22,5 @@ extension View { self } } + #endif } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/LanguagePicker.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/LanguagePicker.swift index 4680d5e9..07c7eb61 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/LanguagePicker.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/LanguagePicker.swift @@ -124,7 +124,9 @@ private struct LanguagePickerList: View { .scrollContentBackground(.hidden) .background(groupedBackgroundColor.edgesIgnoringSafeArea(.all)) .searchable(text: $query) + #if !os(visionOS) .scrollDismissesKeyboard(.interactively) + #endif .navigationTitle("Post Language") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -145,13 +147,23 @@ private struct LanguagePickerList: View { .map { Lang(code: $0) } .sorted { $0.name < $1.name } } + #if os(visionOS) + .onChange(of: query, initial: true) { + filteredLangsChanged(query: query) + } + #else .onChange(of: query) { newValue in - if newValue.isEmpty { - filteredLangs = nil - } else { - filteredLangs = langs.filter { - $0.name.localizedCaseInsensitiveContains(newValue) || $0.code.identifier.localizedCaseInsensitiveContains(newValue) - } + filteredLangsChanged(query: newValue) + } + #endif + } + + private func filteredLangsChanged(query: String) { + if query.isEmpty { + filteredLangs = nil + } else { + filteredLangs = langs.filter { + $0.name.localizedCaseInsensitiveContains(query) || $0.code.identifier.localizedCaseInsensitiveContains(query) } } } diff --git a/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift b/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift index 39135d2f..705ab3da 100644 --- a/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift +++ b/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift @@ -62,7 +62,9 @@ public class DuckableContainerViewController: UIViewController { guard case .idle = state else { if animated, case .ducked(_, placeholder: let placeholder) = state { + #if !os(visionOS) UIImpactFeedbackGenerator(style: .light).impactOccurred() + #endif let origConstant = placeholder.topConstraint.constant UIView.animateKeyframes(withDuration: 0.4, delay: 0) { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) { diff --git a/ShareExtension/ShareHostingController.swift b/ShareExtension/ShareHostingController.swift index abf9fa10..26c01446 100644 --- a/ShareExtension/ShareHostingController.swift +++ b/ShareExtension/ShareHostingController.swift @@ -19,7 +19,11 @@ class ShareHostingController: UIHostingController { let image = UIImage(data: data) else { return nil } + #if os(visionOS) + let size: CGFloat = 50 * 2 + #else let size = 50 * UIScreen.main.scale + #endif return await image.byPreparingThumbnail(ofSize: CGSize(width: size, height: size)) ?? image } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 6c6fafdb..89f50a79 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -85,7 +85,6 @@ D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; }; D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */; }; D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; }; - D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; }; D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; }; D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; }; D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; }; @@ -102,7 +101,7 @@ D635237129B78A7D009ED5E7 /* TuskerComponents in Frameworks */ = {isa = PBXBuildFile; productRef = D635237029B78A7D009ED5E7 /* TuskerComponents */; }; D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; }; D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; }; - D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = D63CC701290EC0B8000E19DE /* Sentry */; }; + D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D63CC701290EC0B8000E19DE /* Sentry */; }; D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */; }; D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */; }; D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */; }; @@ -134,7 +133,7 @@ D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; }; D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; - D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; productRef = D6552366289870790048A653 /* ScreenCorners */; }; + D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6552366289870790048A653 /* ScreenCorners */; }; D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; }; D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; }; D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B532971F71D00DABDFB /* EditedReport.swift */; }; @@ -159,7 +158,6 @@ D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; }; D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; }; D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; }; - D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; }; D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; }; D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; }; D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */; }; @@ -265,7 +263,7 @@ D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; }; D6BD395929B64426005FFD2B /* ComposeUI in Frameworks */ = {isa = PBXBuildFile; productRef = D6BD395829B64426005FFD2B /* ComposeUI */; }; D6BD395B29B64441005FFD2B /* ComposeHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */; }; - D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; }; + D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; }; D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; }; @@ -324,7 +322,7 @@ D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; }; D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; }; D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; }; - D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; }; D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; }; D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; }; @@ -487,7 +485,6 @@ D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = ""; }; D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableViewCell.swift; sourceTree = ""; }; D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = ""; }; - D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = ""; }; D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = ""; }; D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = ""; }; D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = ""; }; @@ -562,7 +559,6 @@ D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = ""; }; D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = ""; }; D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Pachyderm; path = Packages/Pachyderm; sourceTree = ""; }; - D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = ""; }; D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = ""; }; D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = ""; }; @@ -1242,7 +1238,6 @@ D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */, D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */, D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */, - D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */, D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */, D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */, D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */, @@ -1402,7 +1397,6 @@ D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */, D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */, D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */, - D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */, D6A3BC872321F78000FD64D5 /* Account Cell */, D67C57A721E2649B00C3118B /* Account Detail */, D6C7D27B22B6EBE200071952 /* Attachments */, @@ -1868,7 +1862,6 @@ D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */, D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */, D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */, - D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */, D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */, D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */, D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */, @@ -2210,7 +2203,6 @@ D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */, D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */, D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */, - D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */, D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */, D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, @@ -2307,7 +2299,6 @@ }; D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - platformFilter = ios; target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */; targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */; }; @@ -2429,10 +2420,11 @@ OTHER_CODE_SIGN_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,6,7"; }; name = Dist; }; @@ -2495,9 +2487,10 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,6,7"; }; name = Dist; }; @@ -2522,10 +2515,11 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; }; @@ -2550,10 +2544,11 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Release; }; @@ -2578,10 +2573,11 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Dist; }; @@ -2731,12 +2727,13 @@ OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,6,7"; }; name = Debug; }; @@ -2761,10 +2758,11 @@ OTHER_CODE_SIGN_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,6,7"; }; name = Release; }; @@ -2867,9 +2865,10 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,6,7"; }; name = Debug; }; @@ -2892,9 +2891,10 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,6,7"; }; name = Release; }; diff --git a/Tusker/API/FavoriteService.swift b/Tusker/API/FavoriteService.swift index b513c6e9..4bb5c2f5 100644 --- a/Tusker/API/FavoriteService.swift +++ b/Tusker/API/FavoriteService.swift @@ -29,9 +29,11 @@ class FavoriteService { status.favourited.toggle() mastodonController.persistentContainer.statusSubject.send(status.id) + #if !os(visionOS) if hapticFeedback { UIImpactFeedbackGenerator(style: .light).impactOccurred() } + #endif let request = (status.favourited ? Status.favourite : Status.unfavourite)(status.id) do { @@ -49,9 +51,11 @@ class FavoriteService { } presenter.showToast(configuration: config, animated: true) + #if !os(visionOS) if hapticFeedback { UINotificationFeedbackGenerator().notificationOccurred(.error) } + #endif } } diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index a2c8174c..1a56c7c4 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -11,7 +11,9 @@ import Pachyderm import Combine import UserAccounts import InstanceFeatures +#if canImport(Sentry) import Sentry +#endif import ComposeUI private let oauthScopes = [Scope.read, .write, .follow] @@ -96,6 +98,7 @@ class MastodonController: ObservableObject { } .store(in: &cancellables) + #if canImport(Sentry) $instanceInfo .compactMap { $0 } .removeDuplicates(by: { $0.version == $1.version }) @@ -104,6 +107,7 @@ class MastodonController: ObservableObject { setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) } .store(in: &cancellables) + #endif $instance .compactMap { $0 } @@ -579,6 +583,7 @@ class MastodonController: ObservableObject { } +#if canImport(Sentry) private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) { let crumb = Breadcrumb(level: .info, category: "MastodonController") crumb.data = [ @@ -594,3 +599,4 @@ private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) } SentrySDK.addBreadcrumb(crumb) } +#endif diff --git a/Tusker/API/ReblogService.swift b/Tusker/API/ReblogService.swift index aeb936cd..b39de053 100644 --- a/Tusker/API/ReblogService.swift +++ b/Tusker/API/ReblogService.swift @@ -80,9 +80,11 @@ class ReblogService { status.reblogged.toggle() mastodonController.persistentContainer.statusSubject.send(status.id) + #if !os(visionOS) if hapticFeedback { UIImpactFeedbackGenerator(style: .light).impactOccurred() } + #endif let request: Request if status.reblogged { @@ -104,9 +106,11 @@ class ReblogService { } presenter.showToast(configuration: config, animated: true) + #if !os(visionOS) if hapticFeedback { UINotificationFeedbackGenerator().notificationOccurred(.error) } + #endif } } diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 42674b30..4eb1c03e 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -9,7 +9,9 @@ import UIKit import CoreData import OSLog +#if canImport(Sentry) import Sentry +#endif import UserAccounts import ComposeUI import TuskerPreferences @@ -23,9 +25,13 @@ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + #if canImport(Sentry) configureSentry() + #endif + #if !os(visionOS) swizzleStatusBar() swizzlePresentationController() + #endif AppShortcutItem.createItems(for: application) @@ -56,7 +62,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let oldPreferencesFile = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist") if FileManager.default.fileExists(atPath: oldPreferencesFile.path) { if case .failure(let error) = Preferences.migrate(from: oldPreferencesFile) { + #if canImport(Sentry) SentrySDK.capture(error: error) + #endif } } @@ -70,7 +78,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) { DraftsPersistentContainer.shared.migrate(from: url) { if case .failure(let error) = $0 { + #if canImport(Sentry) SentrySDK.capture(error: error) + #endif } } } @@ -81,6 +91,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + #if canImport(Sentry) private func configureSentry() { guard let dsn = Bundle.main.object(forInfoDictionaryKey: "SentryDSN") as? String, !dsn.isEmpty else { @@ -120,9 +131,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)") } } + #endif override func buildMenu(with builder: UIMenuBuilder) { - if builder.system == .main { MenuController.buildMainMenu(builder: builder) } @@ -169,6 +180,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil) } + #if !os(visionOS) private func swizzleStatusBar() { let selector = Selector(("handleTapAction:")) var originalIMP: IMP? @@ -220,5 +232,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Logging.general.error("Unable to swizzle presentation controller") } } + #endif } diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index 9b5cdd05..8259cf94 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -8,6 +8,12 @@ import UIKit +#if os(visionOS) +private let imageScale: CGFloat = 2 +#else +private let imageScale = UIScreen.main.scale +#endif + class ImageCache { static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50)) @@ -26,7 +32,7 @@ class ImageCache { init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) { // todo: might not always want to use UIScreen.main for this, e.g. Catalyst? - let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale)) + let pixelSize = desiredSize?.applying(.init(scaleX: imageScale, y: imageScale)) self.desiredPixelSize = pixelSize self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize) } diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index 26a2b8c8..17d55be7 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -11,7 +11,9 @@ import CoreData import Pachyderm import Combine import OSLog +#if canImport(Sentry) import Sentry +#endif import CloudKit import UserAccounts @@ -199,6 +201,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { try context.save() } catch let error as NSError { logger.error("Unable to save managed object context: \(String(describing: error), privacy: .public)") + #if canImport(Sentry) let crumb = Breadcrumb(level: .fatal, category: "PersistentStore") // note: NSDetailedErrorsKey == "NSDetailedErrorsKey" != "NSDetailedErrors" if let detailed = error.userInfo["NSDetailedErrors"] as? [NSError] { @@ -217,6 +220,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { ] } SentrySDK.addBreadcrumb(crumb) + #endif fatalError("Unable to save managed object context: \(String(describing: error))") } } diff --git a/Tusker/CoreData/TimelinePosition.swift b/Tusker/CoreData/TimelinePosition.swift index 99d164a6..ac29440b 100644 --- a/Tusker/CoreData/TimelinePosition.swift +++ b/Tusker/CoreData/TimelinePosition.swift @@ -86,6 +86,7 @@ func fromTimelineKind(_ kind: String) -> Timeline { // replace with Collection.trimmingPrefix @available(iOS, obsoleted: 16.0) +@available(visionOS 1.0, *) private func trimmingPrefix(_ prefix: String, of str: String) -> Substring { return str[str.index(str.startIndex, offsetBy: prefix.count)...] } diff --git a/Tusker/Extensions/PKDrawing+Render.swift b/Tusker/Extensions/PKDrawing+Render.swift deleted file mode 100644 index 10c2ccaf..00000000 --- a/Tusker/Extensions/PKDrawing+Render.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// PKDrawing+Render.swift -// Tusker -// -// Created by Shadowfacts on 5/9/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import PencilKit - -extension PKDrawing { - - func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage { - let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light) - var drawingImage: UIImage! - lightTraitCollection.performAsCurrent { - drawingImage = self.image(from: rect, scale: scale) - } - - let imageRect = CGRect(origin: .zero, size: rect.size) - let format = UIGraphicsImageRendererFormat() - format.opaque = false - format.scale = scale - let renderer = UIGraphicsImageRenderer(size: rect.size, format: format) - return renderer.image { (context) in - UIColor.white.setFill() - context.fill(imageRect) - drawingImage.draw(in: imageRect) - } - } - -} diff --git a/Tusker/MultiThreadDictionary.swift b/Tusker/MultiThreadDictionary.swift index 2b0147fa..e8b81c32 100644 --- a/Tusker/MultiThreadDictionary.swift +++ b/Tusker/MultiThreadDictionary.swift @@ -12,15 +12,22 @@ import os // once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]> // to make the lock semantics more clear @available(iOS, obsoleted: 16.0) +@available(visionOS 1.0, *) class MultiThreadDictionary { + #if os(visionOS) + private let lock = OSAllocatedUnfairLock(initialState: [Key: Value]()) + #else private let lock: any Lock<[Key: Value]> + #endif init() { + #if !os(visionOS) if #available(iOS 16.0, *) { self.lock = OSAllocatedUnfairLock(initialState: [:]) } else { self.lock = UnfairLock(initialState: [:]) } + #endif } subscript(key: Key) -> Value? { @@ -30,9 +37,15 @@ class MultiThreadDictionary { } } set(value) { + #if os(visionOS) + lock.withLock { dict in + dict[key] = value + } + #else _ = lock.withLock { dict in dict[key] = value } + #endif } } @@ -57,6 +70,7 @@ class MultiThreadDictionary { } } +#if !os(visionOS) // TODO: replace this only with OSAllocatedUnfairLock @available(iOS, obsoleted: 16.0) fileprivate protocol Lock { @@ -87,3 +101,4 @@ fileprivate class UnfairLock: Lock { return try body(&state) } } +#endif diff --git a/Tusker/Preferences/Colors.swift b/Tusker/Preferences/Colors.swift index 59709b52..a3601cd2 100644 --- a/Tusker/Preferences/Colors.swift +++ b/Tusker/Preferences/Colors.swift @@ -81,10 +81,12 @@ extension Color { static let appFill = Color(uiColor: .appFill) } +#if !os(visionOS) @available(iOS, obsoleted: 17.0) private let traitsKey: String = ["Traits", "Defined", "client", "_"].reversed().joined() @available(iOS, obsoleted: 17.0) private let key = "tusker_usePureBlackDarkMode" +#endif @available(iOS 17.0, *) private struct PureBlackDarkModeTrait: UITraitDefinition { @@ -97,10 +99,15 @@ extension UITraitCollection { if #available(iOS 17.0, *) { return self[PureBlackDarkModeTrait.self] } else { + #if os(visionOS) + return true // unreachable + #else return obsoletePureBlackDarkMode + #endif } } + #if !os(visionOS) @available(iOS, obsoleted: 17.0) var obsoletePureBlackDarkMode: Bool { get { @@ -113,13 +120,18 @@ extension UITraitCollection { setValue(dict, forKey: traitsKey) } } + #endif convenience init(pureBlackDarkMode: Bool) { - if #available(iOS 17.0, *) { + if #available(iOS 17.0, visionOS 1.0, *) { self.init(PureBlackDarkModeTrait.self, value: pureBlackDarkMode) } else { self.init() + #if os(visionOS) + // unreachable + #else self.obsoletePureBlackDarkMode = pureBlackDarkMode + #endif } } } diff --git a/Tusker/Scenes/MainSceneDelegate.swift b/Tusker/Scenes/MainSceneDelegate.swift index 984bc8b7..4cc78263 100644 --- a/Tusker/Scenes/MainSceneDelegate.swift +++ b/Tusker/Scenes/MainSceneDelegate.swift @@ -10,7 +10,9 @@ import UIKit import Pachyderm import MessageUI import CoreData +#if canImport(Duckable) import Duckable +#endif import UserAccounts import ComposeUI @@ -245,6 +247,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate mastodonController.initialize() let split = MainSplitViewController(mastodonController: mastodonController) + #if !canImport(Duckable) + return split + #else if UIDevice.current.userInterfaceIdiom == .phone, #available(iOS 16.0, *) { // TODO: maybe the duckable container should be outside the account switching container @@ -252,6 +257,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate } else { return split } + #endif } func createOnboardingUI() -> UIViewController { diff --git a/Tusker/Scenes/TuskerSceneDelegate.swift b/Tusker/Scenes/TuskerSceneDelegate.swift index 25de0c44..ed30e7de 100644 --- a/Tusker/Scenes/TuskerSceneDelegate.swift +++ b/Tusker/Scenes/TuskerSceneDelegate.swift @@ -7,7 +7,9 @@ // import UIKit +#if !os(visionOS) import Sentry +#endif protocol TuskerSceneDelegate: UISceneDelegate { var window: UIWindow? { get } @@ -32,6 +34,9 @@ extension TuskerSceneDelegate { guard let window else { return } window.overrideUserInterfaceStyle = Preferences.shared.theme window.tintColor = Preferences.shared.accentColor.color + #if os(visionOS) + window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode + #else if #available(iOS 17.0, *) { window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode } else { @@ -45,5 +50,6 @@ extension TuskerSceneDelegate { SentrySDK.capture(exception: exception) } } + #endif } } diff --git a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift index b887b031..ee35fc11 100644 --- a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift @@ -45,7 +45,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc var isInteractivelyAnimatingDismissal: Bool = false { didSet { + #if !os(visionOS) setNeedsStatusBarAppearanceUpdate() + #endif } } diff --git a/Tusker/Screens/Compose/ComposeHostingController.swift b/Tusker/Screens/Compose/ComposeHostingController.swift index 2e7b1a86..b874015e 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -14,13 +14,15 @@ import PhotosUI import PencilKit import Pachyderm import CoreData +#if canImport(Duckable) import Duckable +#endif protocol ComposeHostingControllerDelegate: AnyObject { func dismissCompose(mode: DismissMode) -> Bool } -class ComposeHostingController: UIHostingController, DuckableViewController { +class ComposeHostingController: UIHostingController { weak var delegate: ComposeHostingControllerDelegate? @@ -141,8 +143,23 @@ class ComposeHostingController: UIHostingController DuckAttemptAction { if controller.isPosting { return .block @@ -164,21 +181,8 @@ class ComposeHostingController: UIHostingController.self) + #if !os(visionOS) .scrollDismissesKeyboardInteractivelyIfAvailable() + #endif .navigationTitle(create ? "Add Filter" : "Edit Filter") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -169,12 +171,21 @@ struct EditFilterView: View { }, message: { error in Text(error.localizedDescription) }) + #if os(visionOS) + .onChange(of: expiresIn) { + edited = true + if expires.wrappedValue { + filter.expiresIn = expiresIn + } + } + #else .onChange(of: expiresIn, perform: { newValue in edited = true if expires.wrappedValue { filter.expiresIn = newValue } }) + #endif .onReceive(filter.objectWillChange, perform: { _ in edited = true }) diff --git a/Tusker/Screens/Customize Timelines/PinnedTimelinesView.swift b/Tusker/Screens/Customize Timelines/PinnedTimelinesView.swift index e1e3859c..8bcff3cc 100644 --- a/Tusker/Screens/Customize Timelines/PinnedTimelinesView.swift +++ b/Tusker/Screens/Customize Timelines/PinnedTimelinesView.swift @@ -111,6 +111,10 @@ struct PinnedTimelinesView: View { Text("Pinned Timelines") } .sheet(isPresented: $isShowingAddHashtagSheet, content: { + #if os(visionOS) + AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines) + .edgesIgnoringSafeArea(.bottom) + #else if #available(iOS 16.0, *) { AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines) .edgesIgnoringSafeArea(.bottom) @@ -118,6 +122,7 @@ struct PinnedTimelinesView: View { AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines) .edgesIgnoringSafeArea(.bottom) } + #endif }) .sheet(isPresented: $isShowingAddInstanceSheet, content: { AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines) @@ -128,11 +133,19 @@ struct PinnedTimelinesView: View { pinnedTimelines = accountPreferences.pinnedTimelines } } + #if os(visionOS) + .onChange(of: pinnedTimelines) { + if accountPreferences.pinnedTimelines != pinnedTimelines { + accountPreferences.pinnedTimelines = pinnedTimelines + } + } + #else .onChange(of: pinnedTimelines) { newValue in if accountPreferences.pinnedTimelines != newValue { accountPreferences.pinnedTimelines = newValue } } + #endif } } diff --git a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift index b346c7c4..d17cfeeb 100644 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift +++ b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift @@ -108,10 +108,13 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell { } } + // Unneeded on visionOS because there is no light/dark mode + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateLayerColors() } + #endif override func layoutSubviews() { super.layoutSubviews() diff --git a/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift b/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift index e1032463..3c1a5d5b 100644 --- a/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift +++ b/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift @@ -100,10 +100,13 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell { displayNameLabel.updateForAccountDisplayName(account: account) } + // Unneeded on visionOS since there is no light/dark mode + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateLayerColors() } + #endif private func updateLayerColors() { if traitCollection.userInterfaceStyle == .dark { @@ -126,10 +129,12 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell { if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact { toPresent = UINavigationController(rootViewController: host) toPresent.modalPresentationStyle = .pageSheet + #if !os(visionOS) let sheetPresentationController = toPresent.sheetPresentationController! sheetPresentationController.detents = [ .medium() ] + #endif } else { host.modalPresentationStyle = .popover let popoverPresentationController = host.popoverPresentationController! diff --git a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift index dffe8221..3236c8fb 100644 --- a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift +++ b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift @@ -129,10 +129,13 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell { } } + // Unneeded on visionOS because there is no light/dark mode + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateLayerColors() } + #endif private func updateLayerColors() { if traitCollection.userInterfaceStyle == .dark { diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift index 5514444a..faa1d6e4 100644 --- a/Tusker/Screens/Explore/TrendingLinksViewController.swift +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -279,7 +279,9 @@ extension TrendingLinksViewController: UICollectionViewDelegate { } return UIContextMenuConfiguration { let vc = SFSafariViewController(url: url) + #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color + #endif return vc } actionProvider: { _ in UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell))) diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index 0709e6d9..6c83a32f 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -495,6 +495,7 @@ extension TrendsViewController: UICollectionViewDelegate { } @available(iOS, obsoleted: 16.0) + @available(visionOS 1.0, *) func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { guard let item = dataSource.itemIdentifier(for: indexPath) else { return nil @@ -518,7 +519,9 @@ extension TrendsViewController: UICollectionViewDelegate { let cell = collectionView.cellForItem(at: indexPath)! return UIContextMenuConfiguration { let vc = SFSafariViewController(url: url) + #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color + #endif return vc } actionProvider: { _ in UIMenu(children: self.actionsForTrendingLink(card: card, source: .view(cell))) @@ -551,7 +554,7 @@ extension TrendsViewController: UICollectionViewDelegate { } // implementing the highlightPreviewForItemAt method seems to prevent the old, single IndexPath variant of this method from being called on iOS 16 - @available(iOS 16.0, *) + @available(iOS 16.0, visionOS 1.0, *) func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? { guard indexPaths.count == 1 else { return nil diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift index 0104cfab..b804164c 100644 --- a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift @@ -25,7 +25,9 @@ class FastAccountSwitcherViewController: UIViewController { private(set) var accountViews: [FastSwitchingAccountView] = [] private var lastSelectedAccountViewIndex: Int? + #if !os(visionOS) private var selectionChangedFeedbackGenerator: UISelectionFeedbackGenerator? + #endif private var touchBeganFeedbackWorkItem: DispatchWorkItem? var itemOrientation: ItemOrientation = .iconsTrailing @@ -115,7 +117,9 @@ class FastAccountSwitcherViewController: UIViewController { return } lastSelectedAccountViewIndex = nil + #if !os(visionOS) selectionChangedFeedbackGenerator = nil + #endif UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) { self.view.alpha = 0 @@ -160,10 +164,12 @@ class FastAccountSwitcherViewController: UIViewController { private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) { if newIndex == 0 { // add account placeholder + #if !os(visionOS) if hapticFeedback { selectionChangedFeedbackGenerator?.selectionChanged() } selectionChangedFeedbackGenerator = nil + #endif hide() { (self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAddAccount() @@ -172,10 +178,12 @@ class FastAccountSwitcherViewController: UIViewController { let account = UserAccountsManager.shared.accounts[newIndex - 1] if account.id != UserAccountsManager.shared.mostRecentAccountID { + #if !os(visionOS) if hapticFeedback { selectionChangedFeedbackGenerator?.selectionChanged() } selectionChangedFeedbackGenerator = nil + #endif hide() { (self.view.window!.windowScene!.delegate as! MainSceneDelegate).activateAccount(account, animated: true) @@ -191,9 +199,11 @@ class FastAccountSwitcherViewController: UIViewController { @objc private func handleLongPress(_ recognizer: UIGestureRecognizer) { switch recognizer.state { case .began: + #if !os(visionOS) UIImpactFeedbackGenerator(style: .heavy).impactOccurred() selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator() selectionChangedFeedbackGenerator?.prepare() + #endif show() @@ -243,10 +253,12 @@ class FastAccountSwitcherViewController: UIViewController { } lastSelectedAccountViewIndex = selectedAccountViewIndex + #if !os(visionOS) if hapticFeedback { selectionChangedFeedbackGenerator?.selectionChanged() selectionChangedFeedbackGenerator?.prepare() } + #endif } } @@ -269,6 +281,7 @@ class FastAccountSwitcherViewController: UIViewController { accountsStack.bounds.contains(touch.location(in: accountsStack)) { handleGestureMoved(to: touch.location(in: view), hapticFeedback: false) + #if !os(visionOS) // don't trigger the haptic feedback immedaitely // if the user is merely tapping, not initiating a pan, we don't want to trigger a double-impact // if the tap ends very quickly, this will be cancelled @@ -280,6 +293,7 @@ class FastAccountSwitcherViewController: UIViewController { // 100ms determined experimentally to be fast enough that there's not a hugely-perceivable delay when beginning a pan gesture // and slow enough that it's longer than most reasonable-speed taps DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100), execute: touchBeganFeedbackWorkItem!) + #endif } super.touchesBegan(touches, with: event) diff --git a/Tusker/Screens/Large Image/LargeImageContentView.swift b/Tusker/Screens/Large Image/LargeImageContentView.swift index 68f0b7b6..ee59a37e 100644 --- a/Tusker/Screens/Large Image/LargeImageContentView.swift +++ b/Tusker/Screens/Large Image/LargeImageContentView.swift @@ -244,6 +244,10 @@ fileprivate class GifvActivityItemSource: NSObject, UIActivityItemSource { } func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? { + #if os(visionOS) + #warning("Use async AVAssetImageGenerator.image(at:)") + return nil + #else let generator = AVAssetImageGenerator(asset: self.asset) generator.appliesPreferredTrackTransform = true if let image = try? generator.copyCGImage(at: .zero, actualTime: nil) { @@ -251,6 +255,7 @@ fileprivate class GifvActivityItemSource: NSObject, UIActivityItemSource { } else { return nil } + #endif } func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { diff --git a/Tusker/Screens/Large Image/LargeImageViewController.swift b/Tusker/Screens/Large Image/LargeImageViewController.swift index acfed49a..65299289 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LargeImageViewController.swift @@ -54,7 +54,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma var isInteractivelyAnimatingDismissal: Bool = false { didSet { + #if !os(visionOS) setNeedsStatusBarAppearanceUpdate() + #endif } } diff --git a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift index 86da2597..5f81f19b 100644 --- a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift @@ -45,7 +45,9 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie var isInteractivelyAnimatingDismissal: Bool = false { didSet { + #if !os(visionOS) setNeedsStatusBarAppearanceUpdate() + #endif } } diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index e76781c3..267b0578 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -7,7 +7,9 @@ // import UIKit +#if canImport(ScreenCorners) import ScreenCorners +#endif import UserAccounts import ComposeUI @@ -78,11 +80,15 @@ class AccountSwitchingContainerViewController: UIViewController { newRoot.view.transform = CGAffineTransform(translationX: 0, y: newInitialOffset).scaledBy(x: 0.9, y: 0.9) newRoot.view.layer.masksToBounds = true newRoot.view.layer.cornerCurve = .continuous + #if canImport(ScreenCorners) newRoot.view.layer.cornerRadius = view.window?.screen.displayCornerRadius ?? 0 + #endif oldRoot.view.layer.masksToBounds = true oldRoot.view.layer.cornerCurve = .continuous + #if canImport(ScreenCorners) oldRoot.view.layer.cornerRadius = view.window?.screen.displayCornerRadius ?? 0 + #endif // only one edge is affected in each direction, i have no idea why if direction == .upwards { diff --git a/Tusker/Screens/Main/Duckable+Root.swift b/Tusker/Screens/Main/Duckable+Root.swift index 77c8d62b..73e9814b 100644 --- a/Tusker/Screens/Main/Duckable+Root.swift +++ b/Tusker/Screens/Main/Duckable+Root.swift @@ -6,6 +6,8 @@ // Copyright © 2022 Shadowfacts. All rights reserved. // +#if canImport(Duckable) + import UIKit import Duckable import ComposeUI @@ -57,3 +59,5 @@ extension DuckableContainerViewController: AccountSwitchableViewController { (child as? AccountSwitchableViewController)?.isFastAccountSwitcherActive ?? false } } + +#endif diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 3cb9186d..147dcfea 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -19,9 +19,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView! private var fastSwitcherConstraints: [NSLayoutConstraint] = [] - @available(iOS, obsoleted: 16.0) - private var draftToPresentOnAppear: Draft? - var selectedTab: Tab { return Tab(rawValue: selectedIndex)! } @@ -79,21 +76,12 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { } tabBar.isSpringLoaded = true - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - stateRestorationLogger.info("MainTabBarViewController: viewWillAppear, selectedIndex=\(self.selectedIndex, privacy: .public)") - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - stateRestorationLogger.info("MainTabBarViewController: viewDidAppear, selectedIndex=\(self.selectedIndex, privacy: .public)") - if let draftToPresentOnAppear { - self.draftToPresentOnAppear = nil - compose(editing: draftToPresentOnAppear, animated: true) + #if os(visionOS) + registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: Self, previousTraitCollection) in + self.repositionFastSwitcherIndicator() } + #endif } override func viewDidLayoutSubviews() { @@ -105,11 +93,13 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { repositionFastSwitcherIndicator() } + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) repositionFastSwitcherIndicator() } + #endif func select(tab: Tab, dismissPresented: Bool) { if tab == .compose { @@ -189,7 +179,8 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { compose(editing: nil) return false } - if viewController == viewControllers![selectedIndex], + if selectedIndex != NSNotFound, + viewController == viewControllers![selectedIndex], let nav = viewController as? UINavigationController, nav.viewControllers.count == 1, let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController { diff --git a/Tusker/Screens/Notifications/FollowRequestNotificationCollectionViewCell.swift b/Tusker/Screens/Notifications/FollowRequestNotificationCollectionViewCell.swift index 1dc17197..371a7978 100644 --- a/Tusker/Screens/Notifications/FollowRequestNotificationCollectionViewCell.swift +++ b/Tusker/Screens/Notifications/FollowRequestNotificationCollectionViewCell.swift @@ -234,7 +234,9 @@ class FollowRequestNotificationCollectionViewCell: UICollectionViewListCell { do { _ = try await mastodonController.run(request) + #if !os(visionOS) UIImpactFeedbackGenerator(style: .light).impactOccurred() + #endif self.actionButtonsStack.isHidden = true self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label")) } catch let error as Client.Error { @@ -260,7 +262,9 @@ class FollowRequestNotificationCollectionViewCell: UICollectionViewListCell { do { _ = try await mastodonController.run(request) + #if !os(visionOS) UIImpactFeedbackGenerator(style: .light).impactOccurred() + #endif self.actionButtonsStack.isHidden = true self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label")) } catch let error as Client.Error { diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index 02e2ebb2..ffea5a02 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -9,7 +9,9 @@ import UIKit import Pachyderm import Combine +#if canImport(Sentry) import Sentry +#endif class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController { @@ -411,6 +413,7 @@ extension NotificationsCollectionViewController { private func validateNotifications(_ notifications: [Pachyderm.Notification]) -> [Pachyderm.Notification] { return notifications.compactMap { notif in if notif.status == nil && (notif.kind == .mention || notif.kind == .reblog || notif.kind == .favourite || notif.kind == .status) { + #if canImport(Sentry) let crumb = Breadcrumb(level: .fatal, category: "notifications") crumb.data = [ "id": notif.id, @@ -419,6 +422,7 @@ extension NotificationsCollectionViewController { "account": notif.account.id, ] SentrySDK.addBreadcrumb(crumb) + #endif return nil } else { return notif diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index e73d2d84..2e306028 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -64,7 +64,9 @@ class InstanceSelectorTableViewController: UITableViewController { navigationItem.scrollEdgeAppearance = appearance tableView.backgroundColor = .appGroupedBackground + #if !os(visionOS) tableView.keyboardDismissMode = .interactive + #endif tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell) tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 120 diff --git a/Tusker/Screens/Preferences/AppearancePrefsView.swift b/Tusker/Screens/Preferences/AppearancePrefsView.swift index 1f441b16..16ea80eb 100644 --- a/Tusker/Screens/Preferences/AppearancePrefsView.swift +++ b/Tusker/Screens/Preferences/AppearancePrefsView.swift @@ -90,7 +90,8 @@ struct AppearancePrefsView : View { @ViewBuilder private var interfaceSection: some View { - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) + if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) { Section(header: Text("Interface")) { WidescreenNavigationPrefsView() } diff --git a/Tusker/Screens/Preferences/OppositeCollapseKeywordsView.swift b/Tusker/Screens/Preferences/OppositeCollapseKeywordsView.swift index 33443389..27e73746 100644 --- a/Tusker/Screens/Preferences/OppositeCollapseKeywordsView.swift +++ b/Tusker/Screens/Preferences/OppositeCollapseKeywordsView.swift @@ -43,10 +43,13 @@ struct OppositeCollapseKeywordsView: View { .listStyle(.grouped) .appGroupedListBackground(container: PreferencesNavigationController.self) } + #if !os(visionOS) .onAppear(perform: updateAppearance) + #endif .navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords") } + @available(iOS, obsoleted: 16.0) private func updateAppearance() { if #available(iOS 16.0, *) { // no longer necessary diff --git a/Tusker/Screens/Preferences/Tip Jar/TipJarView.swift b/Tusker/Screens/Preferences/Tip Jar/TipJarView.swift index 93e5d880..2b72bc80 100644 --- a/Tusker/Screens/Preferences/Tip Jar/TipJarView.swift +++ b/Tusker/Screens/Preferences/Tip Jar/TipJarView.swift @@ -139,6 +139,10 @@ private struct TipRow: View { @Binding var showConfetti: Bool @State private var error: TipJarView.Error? + #if os(visionOS) + @Environment(\.purchase) private var purchase + #endif + var body: some View { HStack { Text(product.displayName) @@ -175,7 +179,11 @@ private struct TipRow: View { isPurchasing = true let result: Product.PurchaseResult do { + #if os(visionOS) + result = try await purchase(product) + #else result = try await product.purchase() + #endif } catch { self.error = .purchasing(error) isPurchasing = false diff --git a/Tusker/Screens/Report/ReportAddStatusView.swift b/Tusker/Screens/Report/ReportAddStatusView.swift index 05ceaaa7..7ea75315 100644 --- a/Tusker/Screens/Report/ReportAddStatusView.swift +++ b/Tusker/Screens/Report/ReportAddStatusView.swift @@ -77,6 +77,11 @@ private struct ScrollBackgroundModifier: ViewModifier { // even though it is for ReportSelectRulesView?? let traits: UITraitCollection = { var t = UITraitCollection(userInterfaceStyle: colorScheme == .dark ? .dark : .light) + #if os(visionOS) + t = t.modifyingTraits({ mutableTraits in + mutableTraits.pureBlackDarkMode = true + }) + #else if #available(iOS 17.0, *) { t = t.modifyingTraits({ mutableTraits in mutableTraits.pureBlackDarkMode = true @@ -84,6 +89,7 @@ private struct ScrollBackgroundModifier: ViewModifier { } else { t.obsoletePureBlackDarkMode = true } + #endif return t }() Color(uiColor: .appGroupedBackground.resolvedColor(with: traits)) diff --git a/Tusker/Screens/Report/ReportSelectRulesView.swift b/Tusker/Screens/Report/ReportSelectRulesView.swift index 9c10280e..39e6510a 100644 --- a/Tusker/Screens/Report/ReportSelectRulesView.swift +++ b/Tusker/Screens/Report/ReportSelectRulesView.swift @@ -56,6 +56,7 @@ struct ReportSelectRulesView: View { private extension View { @available(iOS, obsoleted: 16.0) + @available(visionOS 1.0, *) @ViewBuilder func withAppBackgroundIfAvailable() -> some View { if #available(iOS 16.0, *) { diff --git a/Tusker/Screens/Report/ReportView.swift b/Tusker/Screens/Report/ReportView.swift index f418bf50..5a255a78 100644 --- a/Tusker/Screens/Report/ReportView.swift +++ b/Tusker/Screens/Report/ReportView.swift @@ -30,7 +30,9 @@ struct ReportView: View { if #available(iOS 16.0, *) { NavigationStack { navigationViewContent + #if !os(visionOS) .scrollDismissesKeyboard(.interactively) + #endif } } else { NavigationView { diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index e4165f08..826a7c99 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -105,7 +105,9 @@ class SearchResultsViewController: UIViewController, CollectionViewController { collectionView.dragDelegate = self collectionView.allowsFocus = true collectionView.backgroundColor = .appGroupedBackground + #if !os(visionOS) collectionView.keyboardDismissMode = .interactive + #endif dataSource = createDataSource() } diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index f27349f2..c8e83cb6 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -9,7 +9,9 @@ import UIKit import Pachyderm import Combine +#if canImport(Sentry) import Sentry +#endif protocol TimelineViewControllerDelegate: AnyObject { func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?) @@ -374,9 +376,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro _ = try await mastodonController.run(req) } catch { stateRestorationLogger.error("TimelineViewController: failed to update timeline marker: \(String(describing: error))") + #if canImport(Sentry) let event = Event(error: error) event.message = SentryMessage(formatted: "Failed to update timeline marker: \(String(describing: error))") SentrySDK.capture(event: event) + #endif } } } @@ -421,9 +425,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro } func restoreStateFromHandoff(statusIDs: [String], centerStatusID: String) async { + #if canImport(Sentry) let crumb = Breadcrumb(level: .debug, category: "TimelineViewController") crumb.message = "Restoring state from handoff activity" SentrySDK.addBreadcrumb(crumb) + #endif await controller.restoreInitial { @MainActor in let position = TimelinePosition(context: mastodonController.persistentContainer.viewContext) position.statusIDs = statusIDs @@ -465,6 +471,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro case .success(let status): statuses.append(status) case .failure(let error): + #if canImport(Sentry) let crumb = Breadcrumb(level: .error, category: "TimelineViewController") crumb.message = "Error loading status" crumb.data = [ @@ -472,15 +479,18 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro "id": id ] SentrySDK.addBreadcrumb(crumb) + #endif } } await mastodonController.persistentContainer.addAll(statuses: statuses, in: mastodonController.persistentContainer.viewContext) // if an icloud sync completed in between starting to load the statuses and finishing, try to load again if position.statusIDs != originalPositionStatusIDs { + #if canImport(Sentry) let crumb = Breadcrumb(level: .info, category: "TimelineViewController") crumb.message = "TimelinePosition statusIDs changed, retrying load" SentrySDK.addBreadcrumb(crumb) + #endif return await loadStatusesToRestore(position: position) } @@ -508,12 +518,14 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro let centerStatusID = position.centerStatusID let items = position.statusIDs.map { Item.status(id: $0, collapseState: .unknown, filterState: .unknown) } snapshot.appendItems(items, toSection: .statuses) + #if canImport(Sentry) let crumb = Breadcrumb(level: .info, category: "TimelineViewController") crumb.message = "Restoring statuses" crumb.data = [ "statusIDs": position.statusIDs ] SentrySDK.addBreadcrumb(crumb) + #endif dataSource.apply(snapshot, animatingDifferences: false) { if let centerStatusID, let index = statusIDs.firstIndex(of: centerStatusID) { @@ -550,9 +562,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro return true } catch { stateRestorationLogger.error("TimelineViewController: failed to load from timeline marker: \(String(describing: error))") + #if canImport(Sentry) let event = Event(error: error) event.message = SentryMessage(formatted: "Failed to load from timeline marker: \(String(describing: error))") SentrySDK.capture(event: event) + #endif return false } } @@ -586,9 +600,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro private func filterResult(state: FilterState, statusID: String) -> (Filterer.Result, NSAttributedString?) { let status = { guard let status = self.mastodonController.persistentContainer.status(for: statusID) else { + #if canImport(Sentry) let crumb = Breadcrumb(level: .fatal, category: "TimelineViewController") crumb.message = "Looking up status \(statusID)" SentrySDK.addBreadcrumb(crumb) + #endif preconditionFailure("Missing status for filtering") } // if the status is a reblog of another one, filter based on that one diff --git a/Tusker/Screens/Utilities/CustomAlertController.swift b/Tusker/Screens/Utilities/CustomAlertController.swift index 4e3f63c7..58bd2429 100644 --- a/Tusker/Screens/Utilities/CustomAlertController.swift +++ b/Tusker/Screens/Utilities/CustomAlertController.swift @@ -149,7 +149,9 @@ class CustomAlertActionsView: UIControl { private var separators: [UIView] = [] private var separatorSizeConstraints: [NSLayoutConstraint] = [] + #if !os(visionOS) private let generator = UISelectionFeedbackGenerator() + #endif private var currentSelectedActionIndex: Int? init(config: CustomAlertController.Configuration, dismiss: @escaping () -> Void) { @@ -297,7 +299,9 @@ class CustomAlertActionsView: UIControl { currentSelectedActionIndex = selectedButton?.offset selectedButton?.element.backgroundColor = .secondarySystemFill + #if !os(visionOS) generator.prepare() + #endif case .changed: if selectedButton == nil && hitTest(recognizer.location(in: self), with: nil)?.tag == ViewTags.customAlertSeparator { @@ -308,13 +312,17 @@ class CustomAlertActionsView: UIControl { if let currentSelectedActionIndex { actionButtons[currentSelectedActionIndex].backgroundColor = nil } + #if !os(visionOS) generator.selectionChanged() + #endif } currentSelectedActionIndex = selectedButton?.offset selectedButton?.element.backgroundColor = .secondarySystemFill + #if !os(visionOS) generator.prepare() + #endif case .ended: if let currentSelectedActionIndex { diff --git a/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift b/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift index 752f0867..ee74cb13 100644 --- a/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift +++ b/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift @@ -15,7 +15,9 @@ class EnhancedNavigationViewController: UINavigationController { var poppedViewControllers = [UIViewController]() var skipResetPoppedOnNextPush = false + #if !os(visionOS) private var interactivePushTransition: InteractivePushTransition! + #endif override var viewControllers: [UIViewController] { didSet { @@ -34,7 +36,9 @@ class EnhancedNavigationViewController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() + #if !os(visionOS) self.interactivePushTransition = InteractivePushTransition(navigationController: self) + #endif if #available(iOS 16.0, *), useBrowserStyleNavigation, @@ -137,6 +141,7 @@ class EnhancedNavigationViewController: UINavigationController { }, animated: true) } + #if !os(visionOS) func onWillShow() { self.transitionCoordinator?.notifyWhenInteractionChanges({ (context) in if context.isCancelled { @@ -154,6 +159,7 @@ class EnhancedNavigationViewController: UINavigationController { } }) } + #endif @available(iOS 16.0, *) private func configureNavItem(_ navItem: UINavigationItem) { diff --git a/Tusker/Screens/Utilities/InteractivePushTransition.swift b/Tusker/Screens/Utilities/InteractivePushTransition.swift index feba5141..132b20a3 100644 --- a/Tusker/Screens/Utilities/InteractivePushTransition.swift +++ b/Tusker/Screens/Utilities/InteractivePushTransition.swift @@ -8,6 +8,8 @@ import UIKit +#if !os(visionOS) + /// Allows interactively moving forward through the navigation stack after popping /// Based on https://github.com/NSExceptional/TBInteractivePushTransition class InteractivePushTransition: UIPercentDrivenInteractiveTransition { @@ -141,3 +143,5 @@ extension InteractivePushTransition: UIViewControllerAnimatedTransitioning { } } } + +#endif diff --git a/Tusker/Shortcuts/UserActivityHandlingContext.swift b/Tusker/Shortcuts/UserActivityHandlingContext.swift index 4bf6e663..9fafb780 100644 --- a/Tusker/Shortcuts/UserActivityHandlingContext.swift +++ b/Tusker/Shortcuts/UserActivityHandlingContext.swift @@ -7,7 +7,9 @@ // import UIKit +#if canImport(Duckable) import Duckable +#endif import ComposeUI @MainActor @@ -105,10 +107,12 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { func finalize(activity: NSUserActivity) { precondition(state > .initial) + #if !os(visionOS) if #available(iOS 16.0, *), let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { self.root.compose(editing: duckedDraft, animated: false, isDucked: true) } + #endif } enum State: Comparable { diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index c4989a25..2c337891 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -51,7 +51,9 @@ extension TuskerNavigationDelegate { let config = SFSafariViewController.Configuration() config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode let vc = SFSafariViewController(url: url, configuration: config) + #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color + #endif present(vc, animated: true) } else if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:]) @@ -92,19 +94,28 @@ extension TuskerNavigationDelegate { func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false) { let draft = draft ?? apiController.createDraft() - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S + if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) { let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id) let options = UIWindowScene.ActivationRequestOptions() + #if os(visionOS) + options.placement = .prominent() + #else options.preferredPresentationStyle = .prominent + #endif UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil) } else { let compose = ComposeHostingController(draft: draft, mastodonController: apiController) + #if os(visionOS) + fatalError("unreachable") + #else if #available(iOS 16.0, *), presentDuckable(compose, animated: animated, isDucked: isDucked) { return } else { present(compose, animated: animated) } + #endif } } diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index fa1b657b..672d06b2 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -226,6 +226,9 @@ class AttachmentView: GIFImageView { let asset = AVURLAsset(url: attachmentURL) let generator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true + #if os(visionOS) + #warning("Use async AVAssetImageGenerator.image(at:)") + #else guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return } UIImage(cgImage: image).prepareForDisplay { [weak self] image in DispatchQueue.main.async { [weak self] in @@ -234,6 +237,7 @@ class AttachmentView: GIFImageView { self.displayImage() } } + #endif } } @@ -275,11 +279,15 @@ class AttachmentView: GIFImageView { AttachmentView.queue.async { let generator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true + #if os(visionOS) + #warning("Use async AVAssetImageGenerator.image(at:)") + #else guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return } DispatchQueue.main.async { self.source = .cgImage(attachmentURL, image) self.displayImage() } + #endif } let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill) diff --git a/Tusker/Views/Attachments/GifvAttachmentView.swift b/Tusker/Views/Attachments/GifvAttachmentView.swift index 73c9bb57..36350e85 100644 --- a/Tusker/Views/Attachments/GifvAttachmentView.swift +++ b/Tusker/Views/Attachments/GifvAttachmentView.swift @@ -47,6 +47,9 @@ class GifvAttachmentView: UIView { private static func createItem(asset: AVAsset) -> AVPlayerItem { let item = AVPlayerItem(asset: asset) if Preferences.shared.grayscaleImages { + #if os(visionOS) + #warning("Use async AVVideoComposition CIFilter initializer") + #else item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { (request) in let filter = CIFilter(name: "CIColorMonochrome")! @@ -56,6 +59,7 @@ class GifvAttachmentView: UIView { request.finish(with: filter.outputImage!, context: nil) }) + #endif } return item } diff --git a/Tusker/Views/BaseEmojiLabel.swift b/Tusker/Views/BaseEmojiLabel.swift index b8f8e551..c275f0d9 100644 --- a/Tusker/Views/BaseEmojiLabel.swift +++ b/Tusker/Views/BaseEmojiLabel.swift @@ -11,6 +11,11 @@ import Pachyderm import WebURLFoundationExtras private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) +#if os(visionOS) +private let imageScale: CGFloat = 2 +#else +private let imageScale = UIScreen.main.scale +#endif protocol BaseEmojiLabel: AnyObject { var emojiIdentifier: AnyHashable? { get set } @@ -45,7 +50,7 @@ extension BaseEmojiLabel { func emojiImageSize(_ image: UIImage) -> CGSize { var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) var scale: CGFloat = 1.4 - scale *= UIScreen.main.scale + scale *= imageScale imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale) return imageSizeMatchingFontSize } @@ -75,7 +80,7 @@ extension BaseEmojiLabel { let cgImage = thumbnail.cgImage { // the thumbnail API takes a pixel size and returns an image with scale 1, but we want the actual screen scale, so convert // see FB12187798 - emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up) + emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: imageScale, orientation: .up) } } else { // otherwise, perform the network request @@ -88,7 +93,7 @@ extension BaseEmojiLabel { } image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in guard let thumbnail = thumbnail?.cgImage, - case let rescaled = UIImage(cgImage: thumbnail, scale: UIScreen.main.scale, orientation: .up), + case let rescaled = UIImage(cgImage: thumbnail, scale: imageScale, orientation: .up), let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else { group.leave() return diff --git a/Tusker/Views/BasicTableViewCell.xib b/Tusker/Views/BasicTableViewCell.xib deleted file mode 100644 index 2698260c..00000000 --- a/Tusker/Views/BasicTableViewCell.xib +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 0a544121..747b2b0c 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -190,7 +190,9 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!) } else if url.scheme == "https" || url.scheme == "http" { let vc = SFSafariViewController(url: url) + #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color + #endif return vc } else { return nil diff --git a/Tusker/Views/Poll/PollOptionsView.swift b/Tusker/Views/Poll/PollOptionsView.swift index ea599b2e..b6d86f8c 100644 --- a/Tusker/Views/Poll/PollOptionsView.swift +++ b/Tusker/Views/Poll/PollOptionsView.swift @@ -28,7 +28,9 @@ class PollOptionsView: UIControl { private let animationDuration: TimeInterval = 0.1 private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95) + #if !os(visionOS) private let generator = UISelectionFeedbackGenerator() + #endif override var isEnabled: Bool { didSet { @@ -135,8 +137,10 @@ class PollOptionsView: UIControl { } animator.startAnimation() + #if !os(visionOS) generator.selectionChanged() generator.prepare() + #endif return true } @@ -168,10 +172,12 @@ class PollOptionsView: UIControl { } } + #if !os(visionOS) if newIndex != nil { generator.selectionChanged() generator.prepare() } + #endif } return true diff --git a/Tusker/Views/Poll/StatusPollView.swift b/Tusker/Views/Poll/StatusPollView.swift index 73d98c08..cfa26b62 100644 --- a/Tusker/Views/Poll/StatusPollView.swift +++ b/Tusker/Views/Poll/StatusPollView.swift @@ -160,7 +160,9 @@ class StatusPollView: UIView, StatusContentPollView { voteButton.isEnabled = false voteButton.disabledTitle = "Voted" + #if !os(visionOS) UIImpactFeedbackGenerator(style: .light).impactOccurred() + #endif let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices) mastodonController.run(request) { (response) in diff --git a/Tusker/Views/Profile Header/ProfileFieldValueView.swift b/Tusker/Views/Profile Header/ProfileFieldValueView.swift index 429eeade..68688f72 100644 --- a/Tusker/Views/Profile Header/ProfileFieldValueView.swift +++ b/Tusker/Views/Profile Header/ProfileFieldValueView.swift @@ -148,10 +148,12 @@ class ProfileFieldValueView: UIView { if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact { toPresent = UINavigationController(rootViewController: host) toPresent.modalPresentationStyle = .pageSheet + #if !os(visionOS) let sheetPresentationController = toPresent.sheetPresentationController! sheetPresentationController.detents = [ .medium() ] + #endif } else { host.modalPresentationStyle = .popover let popoverPresentationController = host.popoverPresentationController! @@ -182,7 +184,9 @@ extension ProfileFieldValueView: UIContextMenuInteractionDelegate, MenuActionPro } else { return UIContextMenuConfiguration { let vc = SFSafariViewController(url: url) + #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color + #endif return vc } actionProvider: { _ in UIMenu(children: self.actionsForURL(url, source: .view(self))) diff --git a/Tusker/Views/Profile Header/ProfileFieldsView.swift b/Tusker/Views/Profile Header/ProfileFieldsView.swift index 7e034134..bc841274 100644 --- a/Tusker/Views/Profile Header/ProfileFieldsView.swift +++ b/Tusker/Views/Profile Header/ProfileFieldsView.swift @@ -55,14 +55,24 @@ class ProfileFieldsView: UIView { boundsObservation = observe(\.bounds, changeHandler: { [unowned self] _, _ in self.setNeedsUpdateConstraints() }) + + #if os(visionOS) + registerForTraitChanges([UITraitHorizontalSizeClass.self, UITraitPreferredContentSizeCategory.self]) { (self: Self, previousTraitCollection) in + if self.isUsingSingleColumn != self.needsSingleColumn { + self.configureFields() + } + } + #endif } + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if isUsingSingleColumn != needsSingleColumn { configureFields() } } + #endif func updateUI(account: AccountMO) { isHidden = account.fields.isEmpty diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.swift b/Tusker/Views/Profile Header/ProfileHeaderView.swift index f6dc76d8..1ea57ab1 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderView.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderView.swift @@ -383,7 +383,9 @@ class ProfileHeaderView: UIView { } followButton.configuration!.showsActivityIndicator = true followButton.isEnabled = false + #if !os(visionOS) UIImpactFeedbackGenerator(style: .light).impactOccurred() + #endif Task { do { let (relationship, _) = try await mastodonController.run(req) diff --git a/Tusker/Views/ScrollingSegmentedControl.swift b/Tusker/Views/ScrollingSegmentedControl.swift index a9d28d73..3e3c195b 100644 --- a/Tusker/Views/ScrollingSegmentedControl.swift +++ b/Tusker/Views/ScrollingSegmentedControl.swift @@ -23,7 +23,9 @@ class ScrollingSegmentedControl: UIScrollView, UIGestureRecogni private var selectedIndicatorViewAlignmentConstraints: [NSLayoutConstraint] = [] private var changeSelectionPanRecognizer: UIGestureRecognizer! private var selectedOptionAtStartOfPan: Value? + #if !os(visionOS) private lazy var selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator() + #endif override var intrinsicContentSize: CGSize { let buttonWidths = optionsStack.arrangedSubviews.map(\.intrinsicContentSize.width).reduce(0, +) @@ -69,18 +71,24 @@ class ScrollingSegmentedControl: UIScrollView, UIGestureRecogni selectedIndicatorView.heightAnchor.constraint(equalToConstant: 4), selectedIndicatorView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor), ]) + + #if os(visionOS) + registerForTraitChanges([UITraitPreferredContentSizeCategory.self], action: #selector(invalidateIntrinsicContentSize)) + #endif } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { invalidateIntrinsicContentSize() } } + #endif private func createOptionViews() { optionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } @@ -107,9 +115,11 @@ class ScrollingSegmentedControl: UIScrollView, UIGestureRecogni return } + #if !os(visionOS) if selectedOption != nil { selectionChangedFeedbackGenerator.selectionChanged() } + #endif selectedOption = value didSelectOption?(value) @@ -170,11 +180,15 @@ class ScrollingSegmentedControl: UIScrollView, UIGestureRecogni switch recognizer.state { case .began: selectedOptionAtStartOfPan = selectedOption + #if !os(visionOS) selectionChangedFeedbackGenerator.prepare() + #endif case .changed: if updateSelectionFor(location: horizontalLocationInStack) { + #if !os(visionOS) selectionChangedFeedbackGenerator.prepare() + #endif } case .ended: @@ -208,7 +222,9 @@ class ScrollingSegmentedControl: UIScrollView, UIGestureRecogni self.layoutIfNeeded() } animator.startAnimation() + #if !os(visionOS) selectionChangedFeedbackGenerator.selectionChanged() + #endif return true } else { return false diff --git a/Tusker/Views/Status/StatusCardView.swift b/Tusker/Views/Status/StatusCardView.swift index 8dce0a7d..378d096e 100644 --- a/Tusker/Views/Status/StatusCardView.swift +++ b/Tusker/Views/Status/StatusCardView.swift @@ -137,10 +137,13 @@ class StatusCardView: UIView { ]) } + // Unneeded on visionOS because there is no light/dark mode + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateBorderColor() } + #endif override func layoutSubviews() { super.layoutSubviews() @@ -240,7 +243,9 @@ extension StatusCardView: UIContextMenuInteractionDelegate { return UIContextMenuConfiguration(identifier: nil) { let vc = SFSafariViewController(url: URL(card.url)!) + #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color + #endif return vc } actionProvider: { (_) in let actions = self.actionProvider?.actionsForURL(URL(card.url)!, source: .view(self)) ?? [] diff --git a/Tusker/Views/Status/StatusMetaIndicatorsView.swift b/Tusker/Views/Status/StatusMetaIndicatorsView.swift index 353182f9..a97c5699 100644 --- a/Tusker/Views/Status/StatusMetaIndicatorsView.swift +++ b/Tusker/Views/Status/StatusMetaIndicatorsView.swift @@ -37,8 +37,20 @@ class StatusMetaIndicatorsView: UIView { private func commonInit() { NotificationCenter.default.addObserver(self, selector: #selector(configureImageViews), name: UIAccessibility.boldTextStatusDidChangeNotification, object: nil) + + #if os(visionOS) + registerForTraitChanges([UITraitPreferredContentSizeCategory.self]) { (self: Self, previousTraitCollection) in + if self.isUsingSingleAxis != self.needsSingleAxis { + for image in self.images { + self.configureImageView(image) + } + self.placeImageViews(self.images) + } + } + #endif } + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if isUsingSingleAxis != needsSingleAxis { @@ -48,6 +60,7 @@ class StatusMetaIndicatorsView: UIView { placeImageViews(images) } } + #endif @objc private func configureImageViews() { for image in images { diff --git a/Tusker/Views/Toast/ToastConfiguration.swift b/Tusker/Views/Toast/ToastConfiguration.swift index dcad6ec4..c44e932c 100644 --- a/Tusker/Views/Toast/ToastConfiguration.swift +++ b/Tusker/Views/Toast/ToastConfiguration.swift @@ -8,7 +8,9 @@ import UIKit import Pachyderm +#if canImport(Sentry) import Sentry +#endif import OSLog @_spi(InstanceType) import InstanceFeatures @@ -96,70 +98,75 @@ fileprivate extension Pachyderm.Client.Error { private let toastErrorLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ToastError") private func captureError(_ error: Client.Error, in mastodonController: MastodonController, title: String) { - let event = Event(error: error) - event.message = SentryMessage(formatted: "\(title): \(error)") - event.tags = [ + var tags = [ "request_method": error.requestMethod.name, "request_endpoint": error.requestEndpoint.description, ] + var extra: [String: String]? switch error.type { case .invalidRequest: - event.tags!["error_type"] = "invalid_request" + tags["error_type"] = "invalid_request" case .invalidResponse: - event.tags!["error_type"] = "invalid_response" + tags["error_type"] = "invalid_response" case .invalidModel(let error): - event.tags!["error_type"] = "invalid_model" - event.extra = [ + tags["error_type"] = "invalid_model" + extra = [ "underlying_error": String(describing: error) ] case .mastodonError(let code, let error): - event.tags!["error_type"] = "mastodon_error" - event.tags!["response_code"] = "\(code)" - event.extra = [ + tags["error_type"] = "mastodon_error" + tags["response_code"] = "\(code)" + extra = [ "underlying_error": String(describing: error) ] case .unexpectedStatus(let code): - event.tags!["error_type"] = "unexpected_status" - event.tags!["response_code"] = "\(code)" + tags["error_type"] = "unexpected_status" + tags["response_code"] = "\(code)" default: return } - if let code = event.tags!["response_code"], + if let code = tags["response_code"], code == "401" || code == "403" || code == "404" || code == "422" || code == "500" || code == "502" || code == "503" { return } switch mastodonController.instanceFeatures.instanceType { case .mastodon(let mastodonType, let mastodonVersion): - event.tags!["instance_type"] = "mastodon" - event.tags!["mastodon_version"] = mastodonVersion?.description ?? "unknown" + tags["instance_type"] = "mastodon" + tags["mastodon_version"] = mastodonVersion?.description ?? "unknown" switch mastodonType { case .vanilla: break case .hometown(_): - event.tags!["mastodon_type"] = "hometown" + tags["mastodon_type"] = "hometown" case .glitch: - event.tags!["mastodon_type"] = "glitch" + tags["mastodon_type"] = "glitch" } case .pleroma(let pleromaType): - event.tags!["instance_type"] = "pleroma" + tags["instance_type"] = "pleroma" switch pleromaType { case .vanilla(let version): - event.tags!["pleroma_version"] = version?.description ?? "unknown" + tags["pleroma_version"] = version?.description ?? "unknown" case .akkoma(let version): - event.tags!["pleroma_type"] = "akkoma" - event.tags!["pleroma_version"] = version?.description ?? "unknown" + tags["pleroma_type"] = "akkoma" + tags["pleroma_version"] = version?.description ?? "unknown" } case .pixelfed: - event.tags!["instance_type"] = "pixelfed" + tags["instance_type"] = "pixelfed" case .gotosocial: - event.tags!["instance_type"] = "gotosocial" + tags["instance_type"] = "gotosocial" case .firefish(let calckeyVersion): - event.tags!["instance_type"] = "firefish" + tags["instance_type"] = "firefish" if let calckeyVersion { - event.tags!["calckey_version"] = calckeyVersion + tags["calckey_version"] = calckeyVersion } } + #if canImport(Sentry) + let event = Event(error: error) + event.message = SentryMessage(formatted: "\(title): \(error)") + event.tags = tags + event.extra = extra SentrySDK.capture(event: event) + #endif - toastErrorLogger.error("\(title, privacy: .public): \(error), \(event.tags!.debugDescription, privacy: .public)") + toastErrorLogger.error("\(title, privacy: .public): \(error), \(tags.debugDescription, privacy: .public)") } diff --git a/Tusker/Views/TrendHistoryView.swift b/Tusker/Views/TrendHistoryView.swift index d18b7f68..f26cfd8d 100644 --- a/Tusker/Views/TrendHistoryView.swift +++ b/Tusker/Views/TrendHistoryView.swift @@ -24,11 +24,14 @@ class TrendHistoryView: UIView { createLayers() } + // Unneeded on visionOS, since there is no dark/light mode + #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) createLayers() } + #endif func setHistory(_ history: [History]?) { if let history = history { From a0eb5dc5967723f71d7276cc8c00ce1a9c644724 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 19 Oct 2023 23:19:28 -0400 Subject: [PATCH 02/15] visionOS: Move Compose toolbar controls to ornament --- .../Controllers/ComposeController.swift | 7 ++ .../Controllers/ToolbarController.swift | 105 ++++++++++-------- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift index 369d89bd..dcdafe5e 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift @@ -315,7 +315,9 @@ public final class ComposeController: ViewController { .transition(.move(edge: .bottom)) .animation(.default, value: controller.currentInput?.autocompleteState) + #if !os(visionOS) ControllerView(controller: { controller.toolbarController }) + #endif } #if !os(visionOS) // on iPadOS15, the toolbar ends up below the keyboard's toolbar without this @@ -335,6 +337,11 @@ public final class ComposeController: ViewController { globalFrameOutsideList = newValue } }) + #if os(visionOS) + .ornament(attachmentAnchor: .scene(.bottom)) { + ControllerView(controller: { controller.toolbarController }) + } + #endif .sheet(isPresented: $controller.isShowingDraftsList) { ControllerView(controller: { DraftsController(parent: controller, isPresented: $controller.isShowingDraftsList) }) } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift index 7ebe6622..5861c987 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift @@ -45,55 +45,27 @@ class ToolbarController: ViewController { @EnvironmentObject private var composeController: ComposeController @ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22 + #if !os(visionOS) @State private var minWidth: CGFloat? @State private var realWidth: CGFloat? + #endif var body: some View { + #if os(visionOS) + buttons + .glassBackgroundEffect(in: .rect(cornerRadius: 50)) + #else ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 0) { - cwButton - - MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly) - // the button has a bunch of extra space by default, but combined with what we add it's too much - .padding(.horizontal, -8) - .disabled(draft.editedStatusID != nil) - .disabled(composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility && draft.localOnly) - - if composeController.mastodonController.instanceFeatures.localOnlyPosts { - localOnlyPicker - .padding(.horizontal, -8) - .disabled(draft.editedStatusID != nil) - } - - if let currentInput = composeController.currentInput, - currentInput.toolbarElements.contains(.emojiPicker) { - customEmojiButton - } - - if let currentInput = composeController.currentInput, - currentInput.toolbarElements.contains(.formattingButtons), - composeController.config.contentType != .plain { - - Spacer() - formatButtons - } - - Spacer() - - if #available(iOS 16.0, *), - composeController.mastodonController.instanceFeatures.createStatusWithLanguage { - LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection) - } - } - .padding(.horizontal, 16) - .frame(minWidth: minWidth) - .background(GeometryReader { proxy in - Color.clear - .preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width) - .onPreferenceChange(ToolbarWidthPrefKey.self) { width in - realWidth = width - } - }) + buttons + .padding(.horizontal, 16) + .frame(minWidth: minWidth) + .background(GeometryReader { proxy in + Color.clear + .preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width) + .onPreferenceChange(ToolbarWidthPrefKey.self) { width in + realWidth = width + } + }) } .scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0) .frame(height: ToolbarController.height) @@ -110,6 +82,51 @@ class ToolbarController: ViewController { minWidth = width } }) + #endif + } + + @ViewBuilder + private var buttons: some View { + HStack(spacing: 0) { + cwButton + + MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly) + #if !os(visionOS) + // the button has a bunch of extra space by default, but combined with what we add it's too much + .padding(.horizontal, -8) + #endif + .disabled(draft.editedStatusID != nil) + .disabled(composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility && draft.localOnly) + + if composeController.mastodonController.instanceFeatures.localOnlyPosts { + localOnlyPicker + #if !os(visionOS) + .padding(.horizontal, -8) + #endif + .disabled(draft.editedStatusID != nil) + } + + if let currentInput = composeController.currentInput, + currentInput.toolbarElements.contains(.emojiPicker) { + customEmojiButton + } + + if let currentInput = composeController.currentInput, + currentInput.toolbarElements.contains(.formattingButtons), + composeController.config.contentType != .plain { + + Spacer() + formatButtons + } + + Spacer() + + if #available(iOS 16.0, *), + composeController.mastodonController.instanceFeatures.createStatusWithLanguage { + LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection) + } + } + .buttonStyle(.borderless) } private var cwButton: some View { From 78196e14c38daf8efe890fdbf3142244f6e82974 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 20 Oct 2023 11:09:44 -0400 Subject: [PATCH 03/15] visionOS: Improve Compose main text view appearance --- .../ComposeUI/Views/MainTextView.swift | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift index df47f4ff..2070e2df 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift @@ -23,19 +23,33 @@ struct MainTextView: View { controller.config } + private var placeholderOffset: CGSize { + #if os(visionOS) + CGSize(width: 8, height: 8) + #else + CGSize(width: 4, height: 8) + #endif + } + var body: some View { ZStack(alignment: .topLeading) { - colorScheme == .dark ? config.fillColor : Color(uiColor: .secondarySystemBackground) + MainWrappedTextViewRepresentable( + text: $draft.text, + backgroundColor: colorScheme == .dark ? UIColor(config.fillColor) : .secondarySystemBackground, + becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, + updateSelection: $updateSelection, + textDidChange: textDidChange + ) if draft.text.isEmpty { ControllerView(controller: { PlaceholderController() }) .font(.system(size: fontSize)) .foregroundColor(.secondary) - .offset(x: 4, y: 8) + .offset(placeholderOffset) .accessibilityHidden(true) + .allowsHitTesting(false) } - MainWrappedTextViewRepresentable(text: $draft.text, becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, updateSelection: $updateSelection, textDidChange: textDidChange) } .frame(height: effectiveHeight) .onAppear(perform: becomeFirstResponderOnFirstAppearance) @@ -62,6 +76,7 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable { typealias UIViewType = UITextView @Binding var text: String + let backgroundColor: UIColor @Binding var becomeFirstResponder: Bool @Binding var updateSelection: ((UITextView) -> Void)? let textDidChange: (UITextView) -> Void @@ -74,10 +89,16 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable { context.coordinator.textView = textView textView.delegate = context.coordinator textView.isEditable = true - textView.backgroundColor = .clear textView.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 20)) textView.adjustsFontForContentSizeCategory = true textView.textContainer.lineBreakMode = .byWordWrapping + + #if os(visionOS) + textView.borderStyle = .roundedRect + // yes, the X inset is 4 less than the placeholder offset + textView.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4) + #endif + return textView } @@ -90,6 +111,8 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable { uiView.isEditable = isEnabled uiView.keyboardType = controller.config.useTwitterKeyboard ? .twitter : .default + uiView.backgroundColor = backgroundColor + context.coordinator.text = $text if let updateSelection { From 27dd8a19272f6b5c27a12256ff38c076382ac310 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 20 Oct 2023 11:14:15 -0400 Subject: [PATCH 04/15] visionOS: Hide light/dark mode prefs --- Tusker/Screens/Preferences/AppearancePrefsView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tusker/Screens/Preferences/AppearancePrefsView.swift b/Tusker/Screens/Preferences/AppearancePrefsView.swift index 16ea80eb..1e59399f 100644 --- a/Tusker/Screens/Preferences/AppearancePrefsView.swift +++ b/Tusker/Screens/Preferences/AppearancePrefsView.swift @@ -56,6 +56,7 @@ struct AppearancePrefsView : View { private var themeSection: some View { Section { + #if !os(visionOS) Picker(selection: $preferences.theme, label: Text("Theme")) { Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified) Text("Light").tag(UIUserInterfaceStyle.light) @@ -68,6 +69,7 @@ struct AppearancePrefsView : View { Text("Pure Black Dark Mode") } } + #endif Picker(selection: $preferences.accentColor, label: Text("Accent Color")) { ForEach(accentColorsAndImages, id: \.0.rawValue) { (color, image) in From 978486bc157b21bf0a69a9101f4ed2e063d63ef6 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 20 Oct 2023 11:23:29 -0400 Subject: [PATCH 05/15] visionOS: Improve button appearance in Compose attachment list --- .../Controllers/AttachmentsListController.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentsListController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentsListController.swift index 15b9b921..611a2dd3 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentsListController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentsListController.swift @@ -131,9 +131,9 @@ class AttachmentsListController: ViewController { @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { + attachmentsList + Group { - attachmentsList - if controller.parent.config.presentAssetPicker != nil { addImageButton .listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) @@ -147,6 +147,10 @@ class AttachmentsListController: ViewController { togglePollButton .listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) } + #if os(visionOS) + .buttonStyle(.bordered) + .labelStyle(AttachmentButtonLabelStyle()) + #endif } private var attachmentsList: some View { @@ -246,3 +250,11 @@ fileprivate struct SheetOrPopover: ViewModifier { } } } + +@available(visionOS 1.0, *) +fileprivate struct AttachmentButtonLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + DefaultLabelStyle().makeBody(configuration: configuration) + .foregroundStyle(.white) + } +} From a93a4fccc14bc84dc61d74b9c328294b4430b910 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Nov 2023 22:31:57 -0500 Subject: [PATCH 06/15] visionOS: Fix timeline jump button appearance --- Tusker/Screens/Timeline/TimelineJumpButton.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Tusker/Screens/Timeline/TimelineJumpButton.swift b/Tusker/Screens/Timeline/TimelineJumpButton.swift index 2f1cf090..46faf97d 100644 --- a/Tusker/Screens/Timeline/TimelineJumpButton.swift +++ b/Tusker/Screens/Timeline/TimelineJumpButton.swift @@ -13,15 +13,23 @@ class TimelineJumpButton: UIView { var action: ((Mode) async -> Void)? override var intrinsicContentSize: CGSize { + #if os(visionOS) + CGSize(width: 44, height: 44) + #else CGSize(width: UIView.noIntrinsicMetric, height: 44) + #endif } private let button: UIButton = { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + #else var config = UIButton.Configuration.plain() - config.image = UIImage(systemName: "arrow.up") - config.contentInsets = .zero // We don't want a background for this button, even when accessibility button shapes are enabled, because it's in the navbar. config.background.backgroundColor = .clear + #endif + config.image = UIImage(systemName: "arrow.up") + config.contentInsets = .zero return UIButton(configuration: config) }() @@ -101,8 +109,7 @@ class TimelineJumpButton: UIView { } self.mode = mode - var config = UIButton.Configuration.plain() - config.contentInsets = .zero + var config = button.configuration! switch mode { case .jump: config.image = UIImage(systemName: "arrow.up") From 9d01bbabd76d66e0aeb3bdf99e6bd736f07a451c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Nov 2023 22:37:07 -0500 Subject: [PATCH 07/15] visionOS: Use UIColor.link for text links --- Tusker/Views/ContentTextView.swift | 6 ++++++ Tusker/Views/Profile Header/ProfileFieldValueView.swift | 4 ++++ Tusker/Views/Status/StatusCardView.swift | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 9a5661f1..29b2d2de 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -78,9 +78,15 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { textContainerInset = .zero textContainer.lineFragmentPadding = 0 + #if os(visionOS) + linkTextAttributes = [ + .foregroundColor: UIColor.link + ] + #else linkTextAttributes = [ .foregroundColor: UIColor.tintColor ] + #endif updateLinkUnderlineStyle() // the text view's builtin link interaction code is tied to isSelectable, so we need to use our own tap recognizer diff --git a/Tusker/Views/Profile Header/ProfileFieldValueView.swift b/Tusker/Views/Profile Header/ProfileFieldValueView.swift index 68688f72..0981343a 100644 --- a/Tusker/Views/Profile Header/ProfileFieldValueView.swift +++ b/Tusker/Views/Profile Header/ProfileFieldValueView.swift @@ -48,7 +48,11 @@ class ProfileFieldValueView: UIView { converted.enumerateAttribute(.link, in: converted.fullRange) { value, range, stop in guard value != nil else { return } + #if os(visionOS) + converted.addAttribute(.foregroundColor, value: UIColor.link, range: range) + #else converted.addAttribute(.foregroundColor, value: UIColor.tintColor, range: range) + #endif // the .link attribute in a UILabel always makes the color blue >.> converted.removeAttribute(.link, range: range) } diff --git a/Tusker/Views/Status/StatusCardView.swift b/Tusker/Views/Status/StatusCardView.swift index 378d096e..60ba2bb1 100644 --- a/Tusker/Views/Status/StatusCardView.swift +++ b/Tusker/Views/Status/StatusCardView.swift @@ -69,7 +69,11 @@ class StatusCardView: UIView { domainLabel.font = .preferredFont(forTextStyle: .caption2) domainLabel.adjustsFontForContentSizeCategory = true domainLabel.numberOfLines = 1 + #if os(visionOS) + domainLabel.textColor = .link + #else domainLabel.textColor = .tintColor + #endif vStack = UIStackView(arrangedSubviews: [ titleLabel, From 19db78e352da539a59ec808563554f9843c1b177 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Nov 2023 22:52:13 -0500 Subject: [PATCH 08/15] visionOS: Don't highlight non-selectable list rows --- .../Conversation/ConversationCollectionViewController.swift | 6 ++++++ .../NotificationsCollectionViewController.swift | 6 ++++++ Tusker/Screens/Profile/ProfileStatusesViewController.swift | 6 ++++++ .../StatusEditHistoryViewController.swift | 6 ++++++ Tusker/Screens/Timeline/TimelineViewController.swift | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift index c0b68430..8b2a790b 100644 --- a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift +++ b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift @@ -385,6 +385,12 @@ extension ConversationCollectionViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self) } + + #if os(visionOS) + func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + return self.collectionView(collectionView, shouldSelectItemAt: indexPath) + } + #endif } extension ConversationCollectionViewController: UICollectionViewDragDelegate { diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index ffea5a02..7aac09c2 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -677,6 +677,12 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate { reconfigureVisibleCells() } } + + #if os(visionOS) + func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + return self.collectionView(collectionView, shouldSelectItemAt: indexPath) + } + #endif } extension NotificationsCollectionViewController: UICollectionViewDragDelegate { diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index cf85c9fa..1b26ab7f 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -628,6 +628,12 @@ extension ProfileStatusesViewController: UICollectionViewDelegate { } } + #if os(visionOS) + func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + return self.collectionView(collectionView, shouldSelectItemAt: indexPath) + } + #endif + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { if reconfigureVisibleItemsOnEndDecelerating { reconfigureVisibleItemsOnEndDecelerating = false diff --git a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift index 8a672a14..ab8fd41e 100644 --- a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift +++ b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift @@ -202,6 +202,12 @@ extension StatusEditHistoryViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { return false } + + #if os(visionOS) + func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + return false + } + #endif } extension StatusEditHistoryViewController: TuskerNavigationDelegate { diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 3c13f02a..a8e52dc4 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -1339,6 +1339,12 @@ extension TimelineViewController: UICollectionViewDelegate { MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self) } + #if os(visionOS) + func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + return self.collectionView(collectionView, shouldSelectItemAt: indexPath) + } + #endif + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if isShowingTimelineDescription { removeTimelineDescriptionCell() From 14f32f24faca6bec33971177f7519a09353f47ea Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 8 Nov 2023 16:37:12 -0500 Subject: [PATCH 09/15] visionOS: Use bordered prominent style for status actions --- ...ersationMainStatusCollectionViewCell.swift | 40 ++++++++++-- .../TimelineStatusCollectionViewCell.swift | 61 +++++++++++++++++-- Tusker/Views/ToggleableButton.swift | 7 ++- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift index ea0fcbb7..f87da664 100644 --- a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift @@ -198,27 +198,51 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status } private(set) lazy var replyButton = UIButton().configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "arrowshape.turn.up.left.fill") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "arrowshape.turn.up.left.fill"), for: .normal) - $0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside) $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside) } private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "star.fill") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "star.fill"), for: .normal) - $0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside) $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside) } private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "repeat") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "repeat"), for: .normal) - $0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside) $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside) } private(set) lazy var moreButton = UIButton().configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "ellipsis") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "ellipsis"), for: .normal) - $0.showsMenuAsPrimaryAction = true $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.showsMenuAsPrimaryAction = true } private var actionButtons: [UIButton] { @@ -233,9 +257,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status ]).configure { $0.axis = .horizontal $0.distribution = .fillEqually + #if os(visionOS) + $0.spacing = 8 + #else NSLayoutConstraint.activate([ $0.heightAnchor.constraint(equalToConstant: 26), ]) + #endif } private let accountDetailToContentWarningSpacer = UIView().configure { @@ -278,8 +306,10 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status contentContainer.widthAnchor.constraint(equalTo: $0.widthAnchor), firstSeparator.widthAnchor.constraint(equalTo: $0.widthAnchor), secondSeparator.widthAnchor.constraint(equalTo: $0.widthAnchor), - actionsHStack.widthAnchor.constraint(equalTo: $0.widthAnchor), ]) + #if !os(visionOS) + actionsHStack.widthAnchor.constraint(equalTo: $0.widthAnchor).isActive = true + #endif } var prevThreadLinkView: UIView? diff --git a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift index d1edccd4..d79ee6ee 100644 --- a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift @@ -205,6 +205,17 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti contentContainer.pollView } + #if os(visionOS) + private lazy var actionsContainer = UIStackView(arrangedSubviews: [ + replyButton, + favoriteButton, + reblogButton, + moreButton, + ]).configure { + $0.axis = .horizontal + $0.spacing = 8 + } + #else private var placeholderReplyButtonLeadingConstraint: NSLayoutConstraint! private lazy var actionsContainer = UIView().configure { replyButton.translatesAutoresizingMaskIntoConstraints = false @@ -239,29 +250,54 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti moreButton.bottomAnchor.constraint(equalTo: $0.bottomAnchor), ]) } + #endif private(set) lazy var replyButton = UIButton().configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "arrowshape.turn.up.left.fill") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "arrowshape.turn.up.left.fill"), for: .normal) - $0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside) $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside) } private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "star.fill") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "star.fill"), for: .normal) - $0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside) $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside) } private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "repeat") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "repeat"), for: .normal) - $0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside) $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside) } private(set) lazy var moreButton = UIButton().configure { + #if os(visionOS) + var config = UIButton.Configuration.borderedProminent() + config.image = UIImage(systemName: "ellipsis") + $0.configuration = config + #else $0.setImage(UIImage(systemName: "ellipsis"), for: .normal) - $0.showsMenuAsPrimaryAction = true $0.addInteraction(UIPointerInteraction(delegate: self)) + #endif + $0.showsMenuAsPrimaryAction = true } private var actionButtons: [UIButton] { @@ -306,7 +342,9 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti private var rebloggerID: String? private var filterReason: String? + #if !os(visionOS) private var firstLayout = true + #endif var isGrayscale = false private var updateTimestampWorkItem: DispatchWorkItem? private var hasCreatedObservers = false @@ -339,14 +377,23 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti mainContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16), mainContainerBottomToActionsConstraint, - actionsContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16), - actionsContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16), // yes, this is deliberately 6. 4 looks to cramped, 8 looks uneven actionsContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6), metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: statusContainer.bottomAnchor, constant: -6), ]) + #if os(visionOS) + NSLayoutConstraint.activate([ + actionsContainer.leadingAnchor.constraint(equalTo: contentVStack.leadingAnchor), + ]) + #else + NSLayoutConstraint.activate([ + actionsContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16), + actionsContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16), + ]) + #endif + updateActionsVisibility() NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) @@ -359,6 +406,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti override func layoutSubviews() { super.layoutSubviews() + #if !os(visionOS) if firstLayout { firstLayout = false @@ -368,6 +416,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti placeholderReplyButtonLeadingConstraint.isActive = false replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true } + #endif } override func updateConfiguration(using state: UICellConfigurationState) { diff --git a/Tusker/Views/ToggleableButton.swift b/Tusker/Views/ToggleableButton.swift index e380b8f9..0984ff5c 100644 --- a/Tusker/Views/ToggleableButton.swift +++ b/Tusker/Views/ToggleableButton.swift @@ -14,7 +14,12 @@ class ToggleableButton: UIButton { var active: Bool { didSet { - tintColor = active ? activeColor : nil + if var config = self.configuration { + config.baseForegroundColor = active ? activeColor : nil + self.configuration = config + } else { + tintColor = active ? activeColor : nil + } } } From c0301ce7e71b098e5243530fe5baabaa693f8a81 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 8 Nov 2023 16:52:46 -0500 Subject: [PATCH 10/15] visionOS: Further Compose screen tweaks --- .../Controllers/AttachmentRowController.swift | 12 ++++++++++-- .../ComposeUI/Controllers/ComposeController.swift | 10 +++++----- .../ComposeUI/Controllers/ToolbarController.swift | 1 - .../Views/AttachmentDescriptionTextView.swift | 14 +++++++++++++- .../Sources/ComposeUI/Views/EmojiTextField.swift | 2 ++ .../Sources/ComposeUI/Views/MainTextView.swift | 12 ++++++++++-- 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift index 16273c85..ff11ef04 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/AttachmentRowController.swift @@ -136,7 +136,7 @@ class AttachmentRowController: ViewController { .overlay { thumbnailFocusedOverlay } - .frame(width: 80, height: 80) + .frame(width: thumbnailSize, height: thumbnailSize) .onTapGesture { textEditorFocused = false // if we just focus the attachment immediately, the text editor doesn't actually unfocus @@ -162,7 +162,7 @@ class AttachmentRowController: ViewController { switch controller.descriptionMode { case .allowEntry: - InlineAttachmentDescriptionView(attachment: attachment, minHeight: 80) + InlineAttachmentDescriptionView(attachment: attachment, minHeight: thumbnailSize) .matchedGeometrySource(id: AttachmentDescriptionTextViewID(attachment), presentationID: attachment.id) .focused($textEditorFocused) @@ -192,6 +192,14 @@ class AttachmentRowController: ViewController { #endif } + private var thumbnailSize: CGFloat { + #if os(visionOS) + 120 + #else + 80 + #endif + } + @ViewBuilder private var thumbnailFocusedOverlay: some View { Image(systemName: "arrow.up.backward.and.arrow.down.forward") diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift index fdf4104d..bb31f3c9 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift @@ -334,6 +334,11 @@ public final class ComposeController: ViewController { .toolbar { ToolbarItem(placement: .cancellationAction) { cancelButton } ToolbarItem(placement: .confirmationAction) { postButton } + #if os(visionOS) + ToolbarItem(placement: .bottomOrnament) { + ControllerView(controller: { controller.toolbarController }) + } + #endif } .background(GeometryReader { proxy in Color.clear @@ -342,11 +347,6 @@ public final class ComposeController: ViewController { globalFrameOutsideList = newValue } }) - #if os(visionOS) - .ornament(attachmentAnchor: .scene(.bottom)) { - ControllerView(controller: { controller.toolbarController }) - } - #endif .sheet(isPresented: $controller.isShowingDraftsList) { ControllerView(controller: { DraftsController(parent: controller, isPresented: $controller.isShowingDraftsList) }) } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift index 5861c987..a81227eb 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift @@ -53,7 +53,6 @@ class ToolbarController: ViewController { var body: some View { #if os(visionOS) buttons - .glassBackgroundEffect(in: .rect(cornerRadius: 50)) #else ScrollView(.horizontal, showsIndicators: false) { buttons diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentDescriptionTextView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentDescriptionTextView.swift index 5f1bbf09..44313e19 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentDescriptionTextView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/AttachmentDescriptionTextView.swift @@ -22,13 +22,21 @@ struct InlineAttachmentDescriptionView: View { self.minHeight = minHeight } + private var placeholderOffset: CGSize { + #if os(visionOS) + CGSize(width: 8, height: 8) + #else + CGSize(width: 4, height: 8) + #endif + } + var body: some View { ZStack(alignment: .topLeading) { if attachment.attachmentDescription.isEmpty { placeholder .font(.body) .foregroundColor(.secondary) - .offset(x: 4, y: 8) + .offset(placeholderOffset) } WrappedTextView( @@ -84,6 +92,10 @@ private struct WrappedTextView: UIViewRepresentable { view.font = .preferredFont(forTextStyle: .body) view.adjustsFontForContentSizeCategory = true view.textContainer.lineBreakMode = .byWordWrapping + #if os(visionOS) + view.borderStyle = .roundedRect + view.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4) + #endif return view } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift index dd02ef8c..cbb9906c 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift @@ -57,7 +57,9 @@ struct EmojiTextField: UIViewRepresentable { context.coordinator.maxLength = maxLength context.coordinator.focusNextView = focusNextView + #if !os(visionOS) uiView.backgroundColor = colorScheme == .dark ? UIColor(controller.config.fillColor) : .secondarySystemBackground + #endif if becomeFirstResponder?.wrappedValue == true { DispatchQueue.main.async { diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift index 2070e2df..23fe44e3 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift @@ -31,11 +31,19 @@ struct MainTextView: View { #endif } + private var textViewBackgroundColor: UIColor? { + #if os(visionOS) + nil + #else + colorScheme == .dark ? UIColor(config.fillColor) : .secondarySystemBackground + #endif + } + var body: some View { ZStack(alignment: .topLeading) { MainWrappedTextViewRepresentable( text: $draft.text, - backgroundColor: colorScheme == .dark ? UIColor(config.fillColor) : .secondarySystemBackground, + backgroundColor: textViewBackgroundColor, becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, updateSelection: $updateSelection, textDidChange: textDidChange @@ -76,7 +84,7 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable { typealias UIViewType = UITextView @Binding var text: String - let backgroundColor: UIColor + let backgroundColor: UIColor? @Binding var becomeFirstResponder: Bool @Binding var updateSelection: ((UITextView) -> Void)? let textDidChange: (UITextView) -> Void From 53302e3b2608440230dd3920c7c830e9861cb806 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 8 Nov 2023 17:05:58 -0500 Subject: [PATCH 11/15] visionOS: Remove trends loading indicator highlight --- Tusker/Screens/Explore/TrendsViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index 6c83a32f..d528579c 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -584,6 +584,12 @@ extension TrendsViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, dismissalPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? { return self.collectionView(collectionView, contextMenuConfiguration: configuration, highlightPreviewForItemAt: indexPath) } + + #if os(visionOS) + func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + return self.collectionView(collectionView, shouldSelectItemAt: indexPath) + } + #endif } extension TrendsViewController: UICollectionViewDragDelegate { From a846954dcd9fec53853d2f2fed122314ac47ecae Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 8 Nov 2023 17:47:01 -0500 Subject: [PATCH 12/15] visionOS: Improve trending link cell appearance --- Tusker.xcodeproj/project.pbxproj | 4 + .../TrendingLinkCardCollectionViewCell.xib | 8 +- .../Explore/TrendingLinkCardView.swift | 95 +++++++++++++++++++ .../Explore/TrendsViewController.swift | 11 +++ 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 Tusker/Screens/Explore/TrendingLinkCardView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index e1076c87..a63ffa47 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -254,6 +254,7 @@ D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.swift */; }; D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366E2828452F00237D0E /* SavedHashtag.swift */; }; D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */; }; + D6BC74842AFC3DF9000DD603 /* TrendingLinkCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC74832AFC3DF9000DD603 /* TrendingLinkCardView.swift */; }; D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; }; D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; }; D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; }; @@ -653,6 +654,7 @@ D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = ""; }; D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = ""; }; D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = ""; }; + D6BC74832AFC3DF9000DD603 /* TrendingLinkCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkCardView.swift; sourceTree = ""; }; D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = ""; }; D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = ""; }; D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = ""; }; @@ -902,6 +904,7 @@ D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */, D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */, + D6BC74832AFC3DF9000DD603 /* TrendingLinkCardView.swift */, D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */, D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */, D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */, @@ -2073,6 +2076,7 @@ D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */, + D6BC74842AFC3DF9000DD603 /* TrendingLinkCardView.swift in Sources */, D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */, D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */, D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */, diff --git a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.xib b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.xib index 3a111877..a5a0e40c 100644 --- a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.xib +++ b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -56,9 +56,9 @@ - + - +