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..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) @@ -177,11 +177,27 @@ 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 + } + + private var thumbnailSize: CGFloat { + #if os(visionOS) + 120 + #else + 80 + #endif } @ViewBuilder @@ -208,6 +224,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/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) + } +} diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift index b31aaec5..bb31f3c9 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift @@ -275,7 +275,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?) { @@ -318,16 +320,25 @@ 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 .padding(.bottom, keyboardInset) + #endif .transition(.move(edge: .bottom)) } } .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 @@ -419,7 +430,9 @@ public final class ComposeController: ViewController { .listRowBackground(config.backgroundColor) } .listStyle(.plain) + #if !os(visionOS) .scrollDismissesKeyboardInteractivelyIfAvailable() + #endif .disabled(controller.isPosting) } @@ -462,6 +475,7 @@ public final class ComposeController: ViewController { } } + #if !os(visionOS) @available(iOS, obsoleted: 16.0) private var keyboardInset: CGFloat { if #unavailable(iOS 16.0), @@ -472,6 +486,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/Controllers/ToolbarController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift index c829b060..71976520 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift @@ -45,61 +45,26 @@ 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 + #else ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 0) { - cwButton - - MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly) - #if !targetEnvironment(macCatalyst) - // 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 targetEnvironment(macCatalyst) - .padding(.leading, 4) - #else - .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) - } - } - .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) @@ -116,6 +81,52 @@ class ToolbarController: ViewController { minWidth = width } }) + #endif + } + + @ViewBuilder + private var buttons: some View { + HStack(spacing: 0) { + cwButton + + MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly) + #if !targetEnvironment(macCatalyst) && !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 targetEnvironment(macCatalyst) + .padding(.leading, 4) + #elseif !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) + } + } } private var cwButton: some View { 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/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/LanguagePicker.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/LanguagePicker.swift index 100e376d..01f252e2 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/LanguagePicker.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/LanguagePicker.swift @@ -129,7 +129,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 { @@ -152,13 +154,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/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift index df47f4ff..23fe44e3 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift @@ -23,19 +23,41 @@ struct MainTextView: View { controller.config } + private var placeholderOffset: CGSize { + #if os(visionOS) + CGSize(width: 8, height: 8) + #else + CGSize(width: 4, height: 8) + #endif + } + + private var textViewBackgroundColor: UIColor? { + #if os(visionOS) + nil + #else + colorScheme == .dark ? UIColor(config.fillColor) : .secondarySystemBackground + #endif + } + var body: some View { ZStack(alignment: .topLeading) { - colorScheme == .dark ? config.fillColor : Color(uiColor: .secondarySystemBackground) + MainWrappedTextViewRepresentable( + text: $draft.text, + backgroundColor: textViewBackgroundColor, + 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 +84,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 +97,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 +119,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 { 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 086c1651..dff8f3d7 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 020d34de..02c5678e 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 */; }; @@ -256,13 +254,15 @@ 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 */; }; + D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.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 */; }; 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 */; }; D6C041C42AED77730094D32D /* EditListSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C041C32AED77730094D32D /* EditListSettingsService.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; @@ -321,7 +321,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 */; }; @@ -484,7 +484,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 = ""; }; @@ -559,7 +558,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 = ""; }; @@ -656,6 +654,8 @@ 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 = ""; }; + D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfileCardView.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 = ""; }; @@ -904,8 +904,10 @@ D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */, D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */, + D6BC74832AFC3DF9000DD603 /* TrendingLinkCardView.swift */, D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */, D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */, + D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */, D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */, D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, @@ -1236,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 */, @@ -1394,7 +1395,6 @@ D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */, D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */, D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */, - D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */, D6A3BC872321F78000FD64D5 /* Account Cell */, D67C57A721E2649B00C3118B /* Account Detail */, D6C7D27B22B6EBE200071952 /* Attachments */, @@ -1858,7 +1858,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 */, @@ -1982,6 +1981,7 @@ D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */, D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */, + D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */, 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */, D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */, @@ -2077,6 +2077,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 */, @@ -2199,7 +2200,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 */, @@ -2295,7 +2295,6 @@ }; D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - platformFilter = ios; target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */; targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */; }; @@ -2418,10 +2417,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; }; @@ -2485,9 +2485,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; }; @@ -2512,10 +2513,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; }; @@ -2540,10 +2542,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; }; @@ -2568,10 +2571,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; }; @@ -2723,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; }; @@ -2753,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; }; @@ -2861,9 +2867,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; }; @@ -2886,9 +2893,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 3d1c1fbd..d11a9999 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] @@ -97,6 +99,7 @@ class MastodonController: ObservableObject { } .store(in: &cancellables) + #if canImport(Sentry) $instanceInfo .compactMap { $0 } .removeDuplicates(by: { $0.version == $1.version }) @@ -105,6 +108,7 @@ class MastodonController: ObservableObject { setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) } .store(in: &cancellables) + #endif $instance .compactMap { $0 } @@ -613,6 +617,7 @@ class MastodonController: ObservableObject { } +#if canImport(Sentry) private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) { let crumb = Breadcrumb(level: .info, category: "MastodonController") crumb.data = [ @@ -628,3 +633,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 710d1d3c..0139e523 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) } @@ -162,6 +173,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } + #if !os(visionOS) private func swizzleStatusBar() { let selector = Selector(("handleTapAction:")) var originalIMP: IMP? @@ -213,5 +225,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 2043411a..9949f5de 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -8,6 +8,13 @@ import UIKit +#if os(visionOS) +private let imageScale: CGFloat = 2 +#else +@MainActor +private let imageScale = UIScreen.main.scale +#endif + final class ImageCache: @unchecked Sendable { @MainActor @@ -31,7 +38,7 @@ final class ImageCache: @unchecked Sendable { @MainActor 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 4c07871a..00000000 --- a/Tusker/Extensions/PKDrawing+Render.swift +++ /dev/null @@ -1,35 +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 { - - @MainActor - func imageInLightMode(from rect: CGRect, scale: CGFloat? = nil) -> UIImage { - let scale = scale ?? UIScreen.main.scale - 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 fde87bd4..8f015d77 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, *) final class MultiThreadDictionary: @unchecked 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 @@ final class MultiThreadDictionary: @u } } 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 @@ final class MultiThreadDictionary: @u } } +#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 dcf37ae2..5976c7eb 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 @@ -252,6 +254,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 @@ -259,6 +264,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 f6a57f7c..a23691e6 100644 --- a/Tusker/Scenes/TuskerSceneDelegate.swift +++ b/Tusker/Scenes/TuskerSceneDelegate.swift @@ -7,7 +7,9 @@ // import UIKit +#if !os(visionOS) import Sentry +#endif @MainActor protocol TuskerSceneDelegate: UISceneDelegate { @@ -33,6 +35,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 { @@ -46,5 +51,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 8efacb63..6ea48cbd 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -14,14 +14,16 @@ import PhotosUI import PencilKit import Pachyderm import CoreData +#if canImport(Duckable) import Duckable +#endif @MainActor protocol ComposeHostingControllerDelegate: AnyObject { func dismissCompose(mode: DismissMode) -> Bool } -class ComposeHostingController: UIHostingController, DuckableViewController { +class ComposeHostingController: UIHostingController { weak var delegate: ComposeHostingControllerDelegate? @@ -142,8 +144,23 @@ class ComposeHostingController: UIHostingController DuckAttemptAction { if controller.isPosting { return .block @@ -165,21 +182,8 @@ class ComposeHostingController: UIHostingController Bool { + return self.collectionView(collectionView, shouldSelectItemAt: indexPath) + } + #endif } extension ConversationCollectionViewController: UICollectionViewDragDelegate { diff --git a/Tusker/Screens/Customize Timelines/AddHashtagPinnedTimelineView.swift b/Tusker/Screens/Customize Timelines/AddHashtagPinnedTimelineView.swift index 060f8002..30e4d53c 100644 --- a/Tusker/Screens/Customize Timelines/AddHashtagPinnedTimelineView.swift +++ b/Tusker/Screens/Customize Timelines/AddHashtagPinnedTimelineView.swift @@ -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) diff --git a/Tusker/Screens/Customize Timelines/EditFilterView.swift b/Tusker/Screens/Customize Timelines/EditFilterView.swift index cd2e5820..acfa70f0 100644 --- a/Tusker/Screens/Customize Timelines/EditFilterView.swift +++ b/Tusker/Screens/Customize Timelines/EditFilterView.swift @@ -148,7 +148,9 @@ struct EditFilterView: View { .appGroupedListRowBackground() } .appGroupedListBackground(container: 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 52a2a8a9..2418e631 100644 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift +++ b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift @@ -106,10 +106,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 c5106ae0..7d2477d7 100644 --- a/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift +++ b/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift @@ -98,10 +98,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 { @@ -124,10 +127,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/SuggestedProfileCardView.swift b/Tusker/Screens/Explore/SuggestedProfileCardView.swift new file mode 100644 index 00000000..40d95bc2 --- /dev/null +++ b/Tusker/Screens/Explore/SuggestedProfileCardView.swift @@ -0,0 +1,78 @@ +// +// SuggestedProfileCardView.swift +// Tusker +// +// Created by Shadowfacts on 11/8/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +#if os(visionOS) +import SwiftUI + +struct SuggestedProfileCardView: View { + let account: AccountMO + + var body: some View { + VStack { + HeaderLayout { + AsyncImage(url: account.header) { image in + image + .resizable() + } placeholder: { + Rectangle().fill(.tertiary) + } + AsyncImage(url: account.avatar) { image in + image + .resizable() + .clipShape(RoundedRectangle(cornerRadius: 5)) + } placeholder: { + Rectangle().fill(.tertiary) + } + VStack(alignment: .leading) { + AccountDisplayNameView(account: account, textStyle: .title, emojiSize: 24) + Text(verbatim: "@\(account.acct)") + } + } + + NoteTextView(note: account.note) + } + .glassBackgroundEffect(in: RoundedRectangle(cornerRadius: 12.5)) + } +} + +private struct HeaderLayout: Layout { + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let acceptedWidth = proposal.width ?? 200 + let avatarSize = subviews[1].sizeThatFits(ProposedViewSize(width: 86, height: 86)) + let accountInfoSize = subviews[2].sizeThatFits(ProposedViewSize(width: acceptedWidth - 8 - avatarSize.width, height: 43)) + return CGSize(width: proposal.width ?? 200, height: 100 + 4 + accountInfoSize.height) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let headerSize = subviews[0].sizeThatFits(ProposedViewSize(width: bounds.width, height: 100)) + subviews[0].place(at: .zero, proposal: ProposedViewSize(headerSize)) + let avatarSize = subviews[1].sizeThatFits(ProposedViewSize(width: 86, height: 86)) + subviews[1].place(at: CGPoint(x: 8, y: headerSize.height), anchor: .leading, proposal: ProposedViewSize(avatarSize)) + subviews[2].place(at: CGPoint(x: 8 + avatarSize.width + 8, y: headerSize.height + 4), proposal: ProposedViewSize(width: bounds.width - 8 - avatarSize.width - 8, height: 43)) + } +} + +private struct NoteTextView: UIViewRepresentable { + typealias UIViewType = ContentTextView + let note: String + func makeUIView(context: Context) -> ContentTextView { + let view = ContentTextView() + view.isUserInteractionEnabled = false + view.isScrollEnabled = true + view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return view + } + func updateUIView(_ uiView: ContentTextView, context: Context) { + uiView.setBodyTextFromHTML(note) + } +} + +//#Preview { +// SuggestedProfileCardView() +//} +#endif diff --git a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift index ae5329eb..0604595d 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/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 @@ - + - +