Compile for visionOS

This commit is contained in:
Shadowfacts 2023-10-19 22:50:20 -04:00
parent c4bf5d406d
commit e4c22a0205
72 changed files with 480 additions and 165 deletions

View File

@ -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)

View File

@ -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<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View {
if #available(iOS 16.0, *) {

View File

@ -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)

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -19,7 +19,11 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
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
}

View File

@ -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 = "<group>"; };
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableViewCell.swift; sourceTree = "<group>"; };
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = "<group>"; };
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = "<group>"; };
@ -562,7 +559,6 @@
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Pachyderm; path = Packages/Pachyderm; sourceTree = "<group>"; };
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = "<group>"; };
@ -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;
};

View File

@ -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
}
}

View File

@ -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

View File

@ -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<Status>
if status.reblogged {
@ -104,9 +106,11 @@ class ReblogService {
}
presenter.showToast(configuration: config, animated: true)
#if !os(visionOS)
if hapticFeedback {
UINotificationFeedbackGenerator().notificationOccurred(.error)
}
#endif
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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))")
}
}

View File

@ -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)...]
}

View File

@ -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)
}
}
}

View File

@ -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<Key: Hashable & Sendable, Value: Sendable> {
#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<Key: Hashable & Sendable, Value: Sendable> {
}
}
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<Key: Hashable & Sendable, Value: Sendable> {
}
}
#if !os(visionOS)
// TODO: replace this only with OSAllocatedUnfairLock
@available(iOS, obsoleted: 16.0)
fileprivate protocol Lock<State> {
@ -87,3 +101,4 @@ fileprivate class UnfairLock<State>: Lock {
return try body(&state)
}
}
#endif

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -45,7 +45,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
#if !os(visionOS)
setNeedsStatusBarAppearanceUpdate()
#endif
}
}

View File

@ -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<ComposeHostingController.View>, DuckableViewController {
class ComposeHostingController: UIHostingController<ComposeHostingController.View> {
weak var delegate: ComposeHostingControllerDelegate?
@ -141,8 +143,23 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
present(ComposeDrawingNavigationController(editing: drawing, delegate: self), animated: true)
}
// MARK: Duckable
struct View: SwiftUI.View {
let mastodonController: MastodonController
let controller: ComposeController
var body: some SwiftUI.View {
ControllerView(controller: { controller })
.task {
if let account = try? await mastodonController.getOwnAccount() {
controller.currentAccount = account
}
}
}
}
}
#if canImport(Duckable)
extension ComposeHostingController: DuckableViewController {
func duckableViewControllerShouldDuck() -> DuckAttemptAction {
if controller.isPosting {
return .block
@ -164,21 +181,8 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
func duckableViewControllerDidFinishAnimatingDuck() {
controller.showToolbar = true
}
struct View: SwiftUI.View {
let mastodonController: MastodonController
let controller: ComposeController
var body: some SwiftUI.View {
ControllerView(controller: { controller })
.task {
if let account = try? await mastodonController.getOwnAccount() {
controller.currentAccount = account
}
}
}
}
}
#endif
extension MastodonController: ComposeMastodonContext {
@MainActor

View File

@ -49,7 +49,9 @@ struct AddHashtagPinnedTimelineView: View {
var body: some View {
NavigationView {
list
#if !os(visionOS)
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
#endif
.listStyle(.grouped)
.navigationTitle("Add Hashtag")
.navigationBarTitleDisplayMode(.inline)

View File

@ -148,7 +148,9 @@ struct EditFilterView: View {
.appGroupedListRowBackground()
}
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.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
})

View File

@ -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
}
}

View File

@ -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()

View File

@ -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!

View File

@ -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 {

View File

@ -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)))

View File

@ -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

View File

@ -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)

View File

@ -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? {

View File

@ -54,7 +54,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
#if !os(visionOS)
setNeedsStatusBarAppearanceUpdate()
#endif
}
}

View File

@ -45,7 +45,9 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
#if !os(visionOS)
setNeedsStatusBarAppearanceUpdate()
#endif
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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, *) {

View File

@ -30,7 +30,9 @@ struct ReportView: View {
if #available(iOS 16.0, *) {
NavigationStack {
navigationViewContent
#if !os(visionOS)
.scrollDismissesKeyboard(.interactively)
#endif
}
} else {
NavigationView {

View File

@ -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()
}

View File

@ -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

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15703"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="xP3-ps-Bp7" style="IBUITableViewCellStyleDefault" id="QwQ-aK-6Xu">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QwQ-aK-6Xu" id="whG-vX-EQq">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xP3-ps-Bp7">
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<point key="canvasLocation" x="-113" y="20"/>
</tableViewCell>
</objects>
</document>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)))

View File

@ -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

View File

@ -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)

View File

@ -23,7 +23,9 @@ class ScrollingSegmentedControl<Value: Hashable>: 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<Value: Hashable>: 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<Value: Hashable>: UIScrollView, UIGestureRecogni
return
}
#if !os(visionOS)
if selectedOption != nil {
selectionChangedFeedbackGenerator.selectionChanged()
}
#endif
selectedOption = value
didSelectOption?(value)
@ -170,11 +180,15 @@ class ScrollingSegmentedControl<Value: Hashable>: 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<Value: Hashable>: UIScrollView, UIGestureRecogni
self.layoutIfNeeded()
}
animator.startAnimation()
#if !os(visionOS)
selectionChangedFeedbackGenerator.selectionChanged()
#endif
return true
} else {
return false

View File

@ -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)) ?? []

View File

@ -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 {

View File

@ -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)")
}

View File

@ -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 {