Revert "Raise min deployment target to iOS 16"

This reverts commit f4b51c06c1.
This commit is contained in:
Shadowfacts 2024-12-15 20:46:05 -05:00
parent a35b72d256
commit 7c43261f9c
66 changed files with 717 additions and 183 deletions

View File

@ -371,13 +371,14 @@ private struct HTMLCallbacks: HTMLConversionCallbacks {
// Converting WebURL to URL is a small but non-trivial expense (since it works by
// serializing the WebURL as a string and then having Foundation parse it again),
// so, if available, use the system parser which doesn't require another round trip.
if let url = try? URL.ParseStrategy().parse(string) {
if #available(iOS 16.0, macOS 13.0, *),
let url = try? URL.ParseStrategy().parse(string) {
url
} else if let web = WebURL(string),
let url = URL(web) {
url
} else {
nil
URL(string: string)
}
}

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "ComposeUI",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -156,7 +156,7 @@ class AttachmentRowController: ViewController {
Button(role: .destructive, action: controller.removeAttachment) {
Label("Delete", systemImage: "trash")
}
} preview: {
} previewIfAvailable: {
ControllerView(controller: { controller.thumbnailController })
}
@ -221,3 +221,16 @@ extension AttachmentRowController {
case allowEntry, recognizingText
}
}
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, *) {
self.contextMenu(menuItems: menuItems, preview: preview)
} else {
self.contextMenu(menuItems: menuItems)
}
}
}

View File

@ -214,6 +214,44 @@ fileprivate extension View {
self
}
}
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View {
if #available(iOS 16.0, *) {
self.modifier(SheetOrPopover(isPresented: isPresented, view: content))
} else {
self.popover(isPresented: isPresented, content: content)
}
}
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func withSheetDetentsIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
} else {
self
}
}
}
@available(iOS 16.0, *)
fileprivate struct SheetOrPopover<V: View>: ViewModifier {
@Binding var isPresented: Bool
@ViewBuilder let view: () -> V
@Environment(\.horizontalSizeClass) var sizeClass
func body(content: Content) -> some View {
if sizeClass == .compact {
content.sheet(isPresented: $isPresented, content: view)
} else {
content.popover(isPresented: $isPresented, content: view)
}
}
}
@available(visionOS 1.0, *)

View File

@ -125,7 +125,9 @@ public final class ComposeController: ViewController {
self.toolbarController = ToolbarController(parent: self)
self.attachmentsListController = AttachmentsListController(parent: self)
if #available(iOS 16.0, *) {
NotificationCenter.default.addObserver(self, selector: #selector(currentInputModeChanged), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
}
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext)
}
@ -322,6 +324,10 @@ public final class ComposeController: ViewController {
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))
}
}
@ -430,7 +436,7 @@ public final class ComposeController: ViewController {
}
.listStyle(.plain)
#if !os(visionOS)
.scrollDismissesKeyboard(.interactively)
.scrollDismissesKeyboardInteractivelyIfAvailable()
#endif
.disabled(controller.isPosting)
}
@ -481,6 +487,31 @@ public final class ComposeController: ViewController {
.keyboardShortcut(.return, modifiers: .command)
.disabled(!controller.postButtonEnabled)
}
#if !os(visionOS)
@available(iOS, obsoleted: 16.0)
private var keyboardInset: CGFloat {
if #unavailable(iOS 16.0),
UIDevice.current.userInterfaceIdiom == .pad,
keyboardReader.isVisible {
return ToolbarController.height
} else {
return 0
}
}
#endif
}
}
private extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self.scrollDismissesKeyboard(.interactively)
} else {
self
}
}
}

View File

@ -51,11 +51,14 @@ class FocusedAttachmentController: ViewController {
.onAppear {
player.play()
}
} else {
} else if #available(iOS 16.0, *) {
ZoomableScrollView {
attachmentView
.matchedGeometryDestination(id: attachment.id)
}
} else {
attachmentView
.matchedGeometryDestination(id: attachment.id)
}
Spacer(minLength: 0)

View File

@ -96,7 +96,7 @@ class PollController: ViewController {
.onMove(perform: controller.moveOptions)
}
.listStyle(.plain)
.scrollDisabled(true)
.scrollDisabledIfAvailable(true)
.frame(height: 44 * CGFloat(poll.options.count))
Button(action: controller.addOption) {

View File

@ -66,7 +66,7 @@ class ToolbarController: ViewController {
}
})
}
.scrollDisabled(realWidth ?? 0 <= minWidth ?? 0)
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
.frame(height: ToolbarController.height)
.frame(maxWidth: .infinity)
.background(.regularMaterial, ignoresSafeAreaEdges: [.bottom, .leading, .trailing])
@ -122,7 +122,8 @@ class ToolbarController: ViewController {
Spacer()
if composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
if #available(iOS 16.0, *),
composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
}
}

View File

@ -10,6 +10,7 @@
import UIKit
import Combine
@available(iOS, obsoleted: 16.0)
class KeyboardReader: ObservableObject {
// @Published var isVisible = false
@Published var keyboardHeight: CGFloat = 0

View File

@ -0,0 +1,26 @@
//
// View+ForwardsCompat.swift
// ComposeUI
//
// Created by Shadowfacts on 3/25/23.
//
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 {
if #available(iOS 16.0, *) {
self.scrollDisabled(disabled)
} else {
self
}
}
#endif
}

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Duckable",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "GalleryVC",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.

View File

@ -9,6 +9,15 @@
import UIKit
import AVFoundation
@propertyWrapper
final class Box<T> {
var wrappedValue: T
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
}
class VideoControlsViewController: UIViewController {
private static let formatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
@ -18,6 +27,9 @@ class VideoControlsViewController: UIViewController {
}()
private let player: AVPlayer
#if !os(visionOS)
@Box private var playbackSpeed: Float
#endif
private lazy var muteButton: MuteButton = {
let button = MuteButton()
@ -51,8 +63,13 @@ class VideoControlsViewController: UIViewController {
private lazy var optionsButton = MenuButton { [unowned self] in
let imageName: String
#if os(visionOS)
let playbackSpeed = player.defaultRate
#else
let playbackSpeed = self.playbackSpeed
#endif
if #available(iOS 17.0, *) {
switch player.defaultRate {
switch playbackSpeed {
case 0.5:
imageName = "gauge.with.dots.needle.0percent"
case 1:
@ -68,8 +85,12 @@ class VideoControlsViewController: UIViewController {
imageName = "speedometer"
}
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
UIAction(title: speed.displayName, state: self.player.defaultRate == speed.rate ? .on : .off) { [unowned self] _ in
UIAction(title: speed.displayName, state: playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
#if os(visionOS)
self.player.defaultRate = speed.rate
#else
self.playbackSpeed = speed.rate
#endif
if self.player.rate > 0 {
self.player.rate = speed.rate
}
@ -99,11 +120,20 @@ class VideoControlsViewController: UIViewController {
private var scrubbingTargetTime: CMTime?
private var isSeeking = false
#if os(visionOS)
init(player: AVPlayer) {
self.player = player
super.init(nibName: nil, bundle: nil)
}
#else
init(player: AVPlayer, playbackSpeed: Box<Float>) {
self.player = player
self._playbackSpeed = playbackSpeed
super.init(nibName: nil, bundle: nil)
}
#endif
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
@ -187,7 +217,11 @@ class VideoControlsViewController: UIViewController {
@objc private func scrubbingEnded() {
scrubbingChanged()
if wasPlayingWhenScrubbingStarted {
#if os(visionOS)
player.play()
#else
player.rate = playbackSpeed
#endif
}
}

View File

@ -16,6 +16,11 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
public private(set) var item: AVPlayerItem
public let player: AVPlayer
#if !os(visionOS)
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
@Box private var playbackSpeed: Float = 1
#endif
private var presentationSizeObservation: NSKeyValueObservation?
private var statusObservation: NSKeyValueObservation?
private var rateObservation: NSKeyValueObservation?
@ -165,12 +170,20 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
isShowingError ? .fade : .fromSourceViewWithoutSnapshot
}
#if os(visionOS)
private lazy var overlayVC = VideoOverlayViewController(player: player)
#else
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
#endif
public var contentOverlayAccessoryViewController: UIViewController? {
overlayVC
}
#if os(visionOS)
public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
#else
public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
#endif
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
if !isShowingError {

View File

@ -15,6 +15,9 @@ class VideoOverlayViewController: UIViewController {
private static let pauseImage = UIImage(systemName: "pause.fill")!
private let player: AVPlayer
#if !os(visionOS)
@Box private var playbackSpeed: Float
#endif
private var dimmingView: UIView!
private var controlsStack: UIStackView!
@ -23,10 +26,18 @@ class VideoOverlayViewController: UIViewController {
private var rateObservation: NSKeyValueObservation?
#if os(visionOS)
init(player: AVPlayer) {
self.player = player
super.init(nibName: nil, bundle: nil)
}
#else
init(player: AVPlayer, playbackSpeed: Box<Float>) {
self.player = player
self._playbackSpeed = playbackSpeed
super.init(nibName: nil, bundle: nil)
}
#endif
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
@ -98,7 +109,11 @@ class VideoOverlayViewController: UIViewController {
if player.currentTime() >= player.currentItem!.duration {
player.seek(to: .zero)
}
#if os(visionOS)
player.play()
#else
player.rate = playbackSpeed
#endif
}
}

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "InstanceFeatures",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "MatchedGeometryPresentation",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Pachyderm",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "PushNotifications",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "TTTKit",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "TuskerComponents",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -9,14 +9,21 @@ import SwiftUI
public struct AsyncPicker<V: Hashable, Content: View>: View {
let titleKey: LocalizedStringKey
#if !os(visionOS)
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
let labelHidden: Bool
#endif
let alignment: Alignment
@Binding var value: V
let onChange: (V) async -> Bool
let content: Content
@State private var isLoading = false
public init(_ titleKey: LocalizedStringKey, alignment: Alignment = .center, value: Binding<V>, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) {
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, alignment: Alignment = .center, value: Binding<V>, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) {
self.titleKey = titleKey
#if !os(visionOS)
self.labelHidden = labelHidden
#endif
self.alignment = alignment
self._value = value
self.onChange = onChange
@ -24,9 +31,25 @@ public struct AsyncPicker<V: Hashable, Content: View>: View {
}
public var body: some View {
#if os(visionOS)
LabeledContent(titleKey) {
picker
}
#else
if #available(iOS 16.0, *) {
LabeledContent(titleKey) {
picker
}
} else if labelHidden {
picker
} else {
HStack {
Text(titleKey)
Spacer()
picker
}
}
#endif
}
private var picker: some View {

View File

@ -10,19 +10,42 @@ import SwiftUI
public struct AsyncToggle: View {
let titleKey: LocalizedStringKey
#if !os(visionOS)
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
let labelHidden: Bool
#endif
@Binding var mode: Mode
let onChange: (Bool) async -> Bool
public init(_ titleKey: LocalizedStringKey, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
self.titleKey = titleKey
#if !os(visionOS)
self.labelHidden = labelHidden
#endif
self._mode = mode
self.onChange = onChange
}
public var body: some View {
#if os(visionOS)
LabeledContent(titleKey) {
toggleOrSpinner
}
#else
if #available(iOS 16.0, *) {
LabeledContent(titleKey) {
toggleOrSpinner
}
} else if labelHidden {
toggleOrSpinner
} else {
HStack {
Text(titleKey)
Spacer()
toggleOrSpinner
}
}
#endif
}
@ViewBuilder

View File

@ -47,7 +47,9 @@ public struct MenuPicker<Value: Hashable>: UIViewRepresentable {
private func makeConfiguration() -> UIButton.Configuration {
var config = UIButton.Configuration.borderless()
if #available(iOS 16.0, *) {
config.indicator = .popup
}
if buttonStyle.hasIcon {
config.image = selectedOption.image
}

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "TuskerPreferences",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "UserAccounts",
platforms: [
.iOS(.v16),
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -141,6 +141,7 @@
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96802BC3279D002C8990 /* PrefsAccountView.swift */; };
D64B96842BC3893C002C8990 /* PushSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */; };
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvPlayerView.swift */; };
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
@ -332,7 +333,7 @@
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F582A13293200AB2315 /* BackgroundManager.swift */; };
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
D6D9498F298EB79400C59229 /* CopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLabel.swift */; };
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
@ -567,6 +568,7 @@
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsAccountView.swift; sourceTree = "<group>"; };
D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscriptionView.swift; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
@ -768,7 +770,7 @@
D6D79F582A13293200AB2315 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
D6D9498E298EB79400C59229 /* CopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLabel.swift; sourceTree = "<group>"; };
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
@ -1476,7 +1478,7 @@
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
D620483523D38075008A63EF /* ContentTextView.swift */,
D6D9498E298EB79400C59229 /* CopyableLabel.swift */,
D6D9498E298EB79400C59229 /* CopyableLable.swift */,
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
@ -1613,6 +1615,7 @@
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */,
D6B81F432560390300F6E31D /* MenuController.swift */,
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */,
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
@ -2287,7 +2290,7 @@
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
D65B4B5A29720AB000DABDFB /* ReportStatusView.swift in Sources */,
D6D9498F298EB79400C59229 /* CopyableLabel.swift in Sources */,
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
@ -2371,6 +2374,7 @@
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */,
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
@ -2527,7 +2531,6 @@
INFOPLIST_FILE = NotificationExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2560,7 +2563,6 @@
INFOPLIST_FILE = NotificationExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2592,7 +2594,6 @@
INFOPLIST_FILE = NotificationExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2683,7 +2684,6 @@
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_FILE = Tusker/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2727,7 +2727,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = TuskerUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2750,7 +2750,6 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2778,7 +2777,6 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2807,7 +2805,6 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2836,7 +2833,6 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2992,7 +2988,6 @@
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_FILE = Tusker/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -3025,7 +3020,6 @@
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_FILE = Tusker/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -3090,7 +3084,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = TuskerUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -3110,7 +3104,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = TuskerUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -3133,7 +3127,6 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -3158,7 +3151,6 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -8,7 +8,6 @@
import Foundation
import CryptoKit
import os
struct DiskCacheTransformer<T> {
let toData: (T) throws -> Data
@ -22,7 +21,7 @@ class DiskCache<T> {
let defaultExpiry: CacheExpiry
let transformer: DiskCacheTransformer<T>
private var fileStates = OSAllocatedUnfairLock(initialState: [String: FileState]())
private var fileStates = MultiThreadDictionary<String, FileState>()
init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
self.defaultExpiry = defaultExpiry
@ -60,9 +59,7 @@ class DiskCache<T> {
}
private func fileState(forKey key: String) -> FileState {
return fileStates.withLock {
$0[key] ?? .unknown
}
return fileStates[key] ?? .unknown
}
func setObject(_ object: T, forKey key: String) throws {
@ -71,17 +68,13 @@ class DiskCache<T> {
guard fileManager.createFile(atPath: path, contents: data, attributes: [.modificationDate: defaultExpiry.date]) else {
throw Error.couldNotCreateFile
}
fileStates.withLock {
$0[key] = .exists
}
fileStates[key] = .exists
}
func removeObject(forKey key: String) throws {
let path = makeFilePath(for: key)
try fileManager.removeItem(atPath: path)
fileStates.withLock {
$0[key] = .doesNotExist
}
fileStates[key] = .doesNotExist
}
func existsObject(forKey key: String) throws -> Bool {
@ -112,9 +105,7 @@ class DiskCache<T> {
}
guard date.timeIntervalSinceNow >= 0 else {
try fileManager.removeItem(atPath: path)
fileStates.withLock {
$0[key] = .doesNotExist
}
fileStates[key] = .doesNotExist
throw Error.expired
}

View File

@ -76,10 +76,17 @@ func fromTimelineKind(_ kind: String) -> Timeline {
} else if kind == "direct" {
return .direct
} else if kind.starts(with: "hashtag:") {
return .tag(hashtag: String(kind.trimmingPrefix("hashtag:")))
return .tag(hashtag: String(trimmingPrefix("hashtag:", of: kind)))
} else if kind.starts(with: "list:") {
return .list(id: String(kind.trimmingPrefix("list:")))
return .list(id: String(trimmingPrefix("list:", of: kind)))
} else {
fatalError("invalid timeline kind \(kind)")
}
}
// 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

@ -36,6 +36,7 @@ private struct AppGroupedListBackground: ViewModifier {
}
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
if colorScheme == .dark, !pureBlackDarkMode {
content
.scrollContentBackground(.hidden)
@ -43,6 +44,12 @@ private struct AppGroupedListBackground: ViewModifier {
} else {
content
}
} else {
content
.onAppear {
UITableView.appearance(whenContainedInInstancesOf: [container]).backgroundColor = .appGroupedBackground
}
}
}
}

View File

@ -48,13 +48,14 @@ extension HTMLConverter {
// Converting WebURL to URL is a small but non-trivial expense (since it works by
// serializing the WebURL as a string and then having Foundation parse it again),
// so, if available, use the system parser which doesn't require another round trip.
if let url = try? URL.ParseStrategy().parse(string) {
if #available(iOS 16.0, macOS 13.0, *),
let url = try? URL.ParseStrategy().parse(string) {
url
} else if let web = WebURL(string),
let url = URL(web) {
url
} else {
nil
URL(string: string)
}
}

View File

@ -0,0 +1,104 @@
//
// MultiThreadDictionary.swift
// Tusker
//
// Created by Shadowfacts on 5/6/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import Foundation
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<Key: Hashable & Sendable, Value: Sendable>: @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? {
get {
return lock.withLock { dict in
dict[key]
}
}
set(value) {
#if os(visionOS)
lock.withLock { dict in
dict[key] = value
}
#else
_ = lock.withLock { dict in
dict[key] = value
}
#endif
}
}
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
func removeValue(forKey key: Key) -> Value? {
return lock.withLock { dict in
dict.removeValue(forKey: key)
}
}
func contains(key: Key) -> Bool {
return lock.withLock { dict in
dict.keys.contains(key)
}
}
// TODO: this should really be throws/rethrows but the stupid type-erased lock makes that not possible
func withLock<R>(_ body: @Sendable (inout [Key: Value]) throws -> R) rethrows -> R where R: Sendable {
return try lock.withLock { dict in
return try body(&dict)
}
}
}
#if !os(visionOS)
// TODO: replace this only with OSAllocatedUnfairLock
@available(iOS, obsoleted: 16.0)
fileprivate protocol Lock<State> {
associatedtype State
func withLock<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R where R: Sendable
}
@available(iOS 16.0, *)
extension OSAllocatedUnfairLock: Lock {
}
// from http://www.russbishop.net/the-law
fileprivate class UnfairLock<State>: Lock {
private var lock: UnsafeMutablePointer<os_unfair_lock>
private var state: State
init(initialState: State) {
self.state = initialState
self.lock = .allocate(capacity: 1)
self.lock.initialize(to: os_unfair_lock())
}
deinit {
self.lock.deinitialize(count: 1)
self.lock.deallocate()
}
func withLock<R>(_ body: (inout State) throws -> R) rethrows -> R where R: Sendable {
os_unfair_lock_lock(lock)
defer { os_unfair_lock_unlock(lock) }
return try body(&state)
}
}
#endif

View File

@ -274,7 +274,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} else {
mainVC = MainSplitViewController(mastodonController: mastodonController)
}
if UIDevice.current.userInterfaceIdiom == .phone {
if UIDevice.current.userInterfaceIdiom == .phone,
#available(iOS 16.0, *) {
// TODO: maybe the duckable container should be outside the account switching container
return DuckableContainerViewController(child: mainVC)
} else {

View File

@ -86,7 +86,7 @@ struct AddReactionView: View {
}
}
.navigationViewStyle(.stack)
.presentationDetents([.medium, .large])
.mediumPresentationDetentIfAvailable()
.alertWithData("Error Adding Reaction", data: $error, actions: { _ in
Button("OK") {}
}, message: { error in
@ -171,6 +171,17 @@ private struct AddReactionButton<Label: View>: View {
}
private extension View {
@available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *)
@ViewBuilder
func mediumPresentationDetentIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self.presentationDetents([.medium, .large])
} else {
self
}
}
@available(iOS, obsoleted: 17.1)
@available(visionOS 1.0, *)
@ViewBuilder

View File

@ -20,10 +20,14 @@ struct AnnouncementListRow: View {
@State private var isShowingAddReactionSheet = false
var body: some View {
if #available(iOS 16.0, *) {
mostOfTheBody
.alignmentGuide(.listRowSeparatorLeading, computeValue: { dimension in
dimension[.leading]
})
} else {
mostOfTheBody
}
}
private var mostOfTheBody: some View {
@ -50,7 +54,11 @@ struct AnnouncementListRow: View {
Label {
Text("Add Reaction")
} icon: {
if #available(iOS 16.0, *) {
Image("face.smiling.badge.plus")
} else {
Image(systemName: "face.smiling")
}
}
}
.labelStyle(.iconOnly)

View File

@ -9,6 +9,20 @@
import SwiftUI
import Pachyderm
@available(iOS, obsoleted: 16.0)
struct AddHashtagPinnedTimelineRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<AddHashtagPinnedTimelineView>
@Binding var pinnedTimelines: [PinnedTimeline]
func makeUIViewController(context: Context) -> UIHostingController<AddHashtagPinnedTimelineView> {
return UIHostingController(rootView: AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines))
}
func updateUIViewController(_ uiViewController: UIHostingController<AddHashtagPinnedTimelineView>, context: Context) {
}
}
struct AddHashtagPinnedTimelineView: View {
@EnvironmentObject private var mastodonController: MastodonController
@Environment(\.dismiss) private var dismiss
@ -35,6 +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

@ -36,9 +36,16 @@ struct CustomizeTimelinesList: View {
}
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
navigationBody
}
} else {
NavigationView {
navigationBody
}
.navigationViewStyle(.stack)
}
}
private var unexpiredFilters: [FilterMO] {

View File

@ -149,7 +149,7 @@ struct EditFilterView: View {
}
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
#if !os(visionOS)
.scrollDismissesKeyboard(.interactively)
.scrollDismissesKeyboardInteractivelyIfAvailable()
#endif
.navigationTitle(create ? "Add Filter" : "Edit Filter")
.navigationBarTitleDisplayMode(.inline)
@ -226,6 +226,18 @@ private struct FilterContextToggleStyle: ToggleStyle {
}
}
private extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self.scrollDismissesKeyboard(.interactively)
} else {
self
}
}
}
//struct EditFilterView_Previews: PreviewProvider {
// static var previews: some View {
// EditFilterView()

View File

@ -115,8 +115,18 @@ struct PinnedTimelinesModifier: ViewModifier {
func body(content: Content) -> some View {
content
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
#if os(visionOS)
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
#else
if #available(iOS 16.0, *) {
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
} else {
AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
}
#endif
})
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)

View File

@ -41,7 +41,9 @@ class InlineTrendsViewController: UIViewController {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
}
let trends = TrendsViewController(mastodonController: mastodonController)
trends.view.translatesAutoresizingMaskIntoConstraints = false

View File

@ -525,12 +525,12 @@ extension TrendsViewController: UICollectionViewDelegate {
}
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
guard indexPaths.count == 1,
let item = dataSource.itemIdentifier(for: indexPaths[0]) else {
@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
}
let indexPath = indexPaths[0]
switch item {
case .loadingIndicator, .confirmLoadMoreStatuses(_):
@ -584,6 +584,15 @@ 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, visionOS 1.0, *)
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
guard indexPaths.count == 1 else {
return nil
}
return self.collectionView(collectionView, contextMenuConfigurationForItemAt: indexPaths[0], point: point)
}
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}

View File

@ -100,6 +100,7 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
navigationItem.renameDelegate = self
@ -108,6 +109,20 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
children.append(contentsOf: self.listSettingsMenuElements())
return UIMenu(children: children)
}
} else {
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Edit", menu: UIMenu(children: [
// uncached so that menu always reflects the current state of the list
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
var elements = self.listSettingsMenuElements()
elements.insert(UIAction(title: "Rename…", image: UIImage(systemName: "pencil"), handler: { [unowned self] _ in
RenameListService(list: self.list, mastodonController: self.mastodonController, present: {
self.present($0, animated: true)
}).run()
}), at: 0)
elementHandler(elements)
})
]))
}
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {

View File

@ -41,9 +41,16 @@ struct MuteAccountView: View {
@State private var error: Error?
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
navigationViewContent
}
} else {
NavigationView {
navigationViewContent
}
.navigationViewStyle(.stack)
}
}
private var navigationViewContent: some View {

View File

@ -101,8 +101,14 @@ extension FollowRequestNotificationViewController: UICollectionViewDelegate {
UIAction(title: "Accept", image: UIImage(systemName: "checkmark.circle"), handler: { _ in cell.acceptButtonPressed() }),
UIAction(title: "Reject", image: UIImage(systemName: "xmark.circle"), handler: { _ in cell.rejectButtonPressed() }),
]
let acceptRejectMenu: UIMenu
if #available(iOS 16.0, *) {
acceptRejectMenu = UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren)
} else {
acceptRejectMenu = UIMenu(options: .displayInline, children: acceptRejectChildren)
}
return UIMenu(children: [
UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren),
acceptRejectMenu,
UIMenu(options: .displayInline, children: self.actionsForProfile(accountID: accountID, source: .view(cell))),
])
}

View File

@ -700,8 +700,14 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
UIAction(title: "Accept", image: UIImage(systemName: "checkmark.circle"), handler: { _ in cell.acceptButtonPressed() }),
UIAction(title: "Reject", image: UIImage(systemName: "xmark.circle"), handler: { _ in cell.rejectButtonPressed() }),
]
let acceptRejectMenu: UIMenu
if #available(iOS 16.0, *) {
acceptRejectMenu = UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren)
} else {
acceptRejectMenu = UIMenu(options: .displayInline, children: acceptRejectChildren)
}
return UIMenu(children: [
UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren),
acceptRejectMenu,
UIMenu(options: .displayInline, children: self.actionsForProfile(accountID: accountID, source: .view(cell))),
])
}

View File

@ -96,7 +96,9 @@ class InstanceSelectorTableViewController: UITableViewController {
searchController.searchBar.placeholder = "Search or enter a URL"
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
}
definesPresentationContext = true
urlHandler = urlCheckerSubject

View File

@ -91,11 +91,15 @@ struct AboutView: View {
@ViewBuilder
private var iconOrGame: some View {
if #available(iOS 16.0, *) {
FlipView {
appIcon
} back: {
TTTView()
}
} else {
appIcon
}
}
private var appIcon: some View {

View File

@ -27,7 +27,14 @@ struct AppearancePrefsView: View {
private static let accentColorsAndImages: [(AccentColor, UIImage?)] = AccentColor.allCases.map { color in
var image: UIImage?
if let color = color.color {
if #available(iOS 16.0, *) {
image = UIImage(systemName: "circle.fill")!.withTintColor(color, renderingMode: .alwaysTemplate).withRenderingMode(.alwaysOriginal)
} else {
image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 20)).image { context in
color.setFill()
context.cgContext.fillEllipse(in: CGRect(x: 0, y: 0, width: 20, height: 20))
}
}
}
return (color, image)
}

View File

@ -36,7 +36,12 @@ struct NotificationsPrefsView: View {
if #available(iOS 15.4, *) {
Section {
Button {
if let url = URL(string: UIApplication.openNotificationSettingsURLString) {
let str = if #available(iOS 16.0, *) {
UIApplication.openNotificationSettingsURLString
} else {
UIApplicationOpenNotificationSettingsURLString
}
if let url = URL(string: str) {
UIApplication.shared.open(url)
}
} label: {

View File

@ -34,7 +34,7 @@ struct PushInstanceSettingsView: View {
HStack {
PrefsAccountView(account: account)
Spacer()
AsyncToggle("\(account.instanceURL.host!) notifications enabled", mode: $mode, onChange: updateNotificationsEnabled(enabled:))
AsyncToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:))
.labelsHidden()
}
PushSubscriptionView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription)

View File

@ -43,9 +43,21 @@ 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
} else {
UIScrollView.appearance(whenContainedInInstancesOf: [PreferencesNavigationController.self]).keyboardDismissMode = .interactive
}
}
private func commitExisting(at index: Int) -> () -> Void {
return {
if keywords[index].value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {

View File

@ -69,6 +69,7 @@ private struct ScrollBackgroundModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.scrollContentBackground(.hidden)
.background {
@ -94,5 +95,8 @@ private struct ScrollBackgroundModifier: ViewModifier {
Color(uiColor: .appGroupedBackground.resolvedColor(with: traits))
.edgesIgnoringSafeArea(.all)
}
} else {
content
}
}
}

View File

@ -49,9 +49,23 @@ struct ReportSelectRulesView: View {
}
.appGroupedListRowBackground()
}
.withAppBackgroundIfAvailable()
.navigationTitle("Rules")
}
}
private extension View {
@available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *)
@ViewBuilder
func withAppBackgroundIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self
.scrollContentBackground(.hidden)
.background(Color.appGroupedBackground)
.navigationTitle("Rules")
} else {
self
}
}
}

View File

@ -27,12 +27,19 @@ struct ReportView: View {
}
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
navigationViewContent
#if !os(visionOS)
.scrollDismissesKeyboard(.interactively)
#endif
}
} else {
NavigationView {
navigationViewContent
}
.navigationViewStyle(.stack)
}
}
private var navigationViewContent: some View {

View File

@ -39,7 +39,9 @@ class MastodonSearchController: UISearchController {
searchResultsUpdater = searchResultsController
automaticallyShowsSearchResultsController = false
showsSearchResultsController = true
if #available(iOS 16.0, *) {
scopeBarActivation = .onSearchActivation
}
searchBar.autocapitalizationType = .none
searchBar.delegate = self
@ -76,9 +78,13 @@ class MastodonSearchController: UISearchController {
if searchText != defaultLanguage,
let match = languageRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.utf16.count)) {
let identifier = (searchText as NSString).substring(with: match.range(at: 1))
if #available(iOS 16.0, *) {
if Locale.LanguageCode.isoLanguageCodes.contains(where: { $0.identifier == identifier }) {
langSuggestions.append("language:\(identifier)")
}
} else if searchText != "en" {
langSuggestions.append("language:\(searchText)")
}
}
suggestions.append((.language, langSuggestions))

View File

@ -22,7 +22,8 @@ class EnhancedNavigationViewController: UINavigationController {
override var viewControllers: [UIViewController] {
didSet {
poppedViewControllers = []
if useBrowserStyleNavigation {
if #available(iOS 16.0, *),
useBrowserStyleNavigation {
// TODO: this for loop might not be necessary
for vc in viewControllers {
configureNavItem(vc.navigationItem)
@ -39,7 +40,8 @@ class EnhancedNavigationViewController: UINavigationController {
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
#endif
if useBrowserStyleNavigation,
if #available(iOS 16.0, *),
useBrowserStyleNavigation,
let topViewController {
configureNavItem(topViewController.navigationItem)
updateTopNavItemState()
@ -50,7 +52,9 @@ class EnhancedNavigationViewController: UINavigationController {
let popped = performAfterAnimating(block: {
super.popViewController(animated: animated)
}, after: {
if #available(iOS 16.0, *) {
self.updateTopNavItemState()
}
}, animated: animated)
if let popped {
poppedViewControllers.insert(popped, at: 0)
@ -62,7 +66,9 @@ class EnhancedNavigationViewController: UINavigationController {
let popped = performAfterAnimating(block: {
super.popToRootViewController(animated: animated)
}, after: {
if #available(iOS 16.0, *) {
self.updateTopNavItemState()
}
}, animated: animated)
if let popped {
poppedViewControllers = popped
@ -74,7 +80,9 @@ class EnhancedNavigationViewController: UINavigationController {
let popped = performAfterAnimating(block: {
super.popToViewController(viewController, animated: animated)
}, after: {
if #available(iOS 16.0, *) {
self.updateTopNavItemState()
}
}, animated: animated)
if let popped {
poppedViewControllers.insert(contentsOf: popped, at: 0)
@ -89,12 +97,16 @@ class EnhancedNavigationViewController: UINavigationController {
self.poppedViewControllers = []
}
if #available(iOS 16.0, *) {
configureNavItem(viewController.navigationItem)
}
super.pushViewController(viewController, animated: animated)
if #available(iOS 16.0, *) {
updateTopNavItemState()
}
}
func pushPoppedViewController() {
guard !poppedViewControllers.isEmpty else {
@ -123,7 +135,9 @@ class EnhancedNavigationViewController: UINavigationController {
pushViewController(target, animated: true)
}, after: {
self.viewControllers.insert(contentsOf: toInsert, at: self.viewControllers.count - 1)
if #available(iOS 16.0, *) {
self.updateTopNavItemState()
}
}, animated: true)
}

View File

@ -204,7 +204,8 @@ extension MenuActionProvider {
}),
]
if includeStatusButtonActions {
if #available(iOS 16.0, *),
includeStatusButtonActions {
let favorited = status.favourited
// TODO: move this color into an asset catalog or something
var favImage = UIImage(systemName: favorited ? "star.fill" : "star")!
@ -367,11 +368,19 @@ extension MenuActionProvider {
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
if #available(iOS 16.0, *) {
let toggleableAndActions = toggleableSection + actionsSection
return [
UIMenu(options: .displayInline, preferredElementSize: toggleableAndActions.count == 1 ? .large : .medium, children: toggleableAndActions),
UIMenu(options: .displayInline, children: shareSection),
]
} else {
return [
UIMenu(options: .displayInline, children: shareSection),
UIMenu(options: .displayInline, children: toggleableSection),
UIMenu(options: .displayInline, children: actionsSection),
]
}
}
func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {

View File

@ -108,7 +108,8 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
}
func compose(editing draft: Draft) {
if UIDevice.current.userInterfaceIdiom == .phone {
if #available(iOS 16.0, *),
UIDevice.current.userInterfaceIdiom == .phone {
self.root.compose(editing: draft, animated: false, isDucked: true, completion: nil)
} else {
DispatchQueue.main.async {
@ -122,7 +123,8 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
precondition(state > .initial)
navigation.run()
#if !os(visionOS)
if let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
if #available(iOS 16.0, *),
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
self.root.compose(editing: duckedDraft, animated: false, isDucked: true, completion: nil)
}
#endif

View File

@ -114,7 +114,8 @@ extension TuskerNavigationDelegate {
#if os(visionOS)
fatalError("unreachable")
#else
if presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
if #available(iOS 16.0, *),
presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
return
} else {
present(compose, animated: animated, completion: completion)

View File

@ -9,7 +9,6 @@
import SwiftUI
import Pachyderm
import WebURLFoundationExtras
import os
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@ -41,7 +40,7 @@ struct AccountDisplayNameView: View {
guard !matches.isEmpty else { return }
let emojiSize = self.emojiSize
let emojiImages = OSAllocatedUnfairLock(initialState: [String: Image]())
let emojiImages = MultiThreadDictionary<String, Image>()
let group = DispatchGroup()
@ -64,9 +63,7 @@ struct AccountDisplayNameView: View {
image.draw(in: CGRect(origin: .zero, size: size))
}
emojiImages.withLock {
$0[emoji.shortcode] = Image(uiImage: resized)
}
emojiImages[emoji.shortcode] = Image(uiImage: resized)
}
if let request = request {
emojiRequests.append(request)
@ -81,7 +78,7 @@ struct AccountDisplayNameView: View {
// iterate backwards as to not alter the indices of earlier matches
for match in matches.reversed() {
let shortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
guard let image = emojiImages.withLock({ $0[shortcode] }) else { continue }
guard let image = emojiImages[shortcode] else { continue }
let afterCurrentMatch = (account.displayName as NSString).substring(with: NSRange(location: match.range.upperBound, length: endIndex - match.range.upperBound))

View File

@ -263,7 +263,17 @@ class AttachmentView: GIFImageView {
let asset = AVURLAsset(url: attachment.url)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
guard let image = try? await generator.image(at: .zero).image,
let image: CGImage?
#if os(visionOS)
image = try? await generator.image(at: .zero).image
#else
if #available(iOS 16.0, *) {
image = try? await generator.image(at: .zero).image
} else {
image = try? generator.copyCGImage(at: .zero, actualTime: nil)
}
#endif
guard let image,
let prepared = await UIImage(cgImage: image).byPreparingForDisplay(),
!Task.isCancelled else {
return

View File

@ -9,7 +9,6 @@
import UIKit
import Pachyderm
import WebURLFoundationExtras
import os
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@ -57,7 +56,7 @@ extension BaseEmojiLabel {
return imageSizeMatchingFontSize
}
let emojiImages = OSAllocatedUnfairLock(initialState: [String: UIImage]())
let emojiImages = MultiThreadDictionary<String, UIImage>()
var foundEmojis = false
let group = DispatchGroup()
@ -80,11 +79,9 @@ extension BaseEmojiLabel {
// todo: consider caching these somewhere? the cache needs to take into account both the url and the font size, which may vary across labels, so can't just use ImageCache
if let thumbnail = image.preparingThumbnail(of: emojiImageSize(image)),
let cgImage = thumbnail.cgImage {
emojiImages.withLock {
// 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
$0[emoji.shortcode] = UIImage(cgImage: cgImage, scale: screenScale, orientation: .up)
}
emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: screenScale, orientation: .up)
}
} else {
// otherwise, perform the network request
@ -102,9 +99,7 @@ extension BaseEmojiLabel {
group.leave()
return
}
emojiImages.withLock {
$0[emoji.shortcode] = transformedImage
}
emojiImages[emoji.shortcode] = transformedImage
group.leave()
}
}

View File

@ -146,7 +146,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? {
let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
if let textLayoutManager {
if #available(iOS 16.0, *),
let textLayoutManager {
guard let fragment = textLayoutManager.textLayoutFragment(for: locationInTextContainer) else {
return nil
}
@ -304,7 +305,8 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
// Determine the line rects that the link takes up in the coordinate space of this view.
var rects = [CGRect]()
if let textLayoutManager,
if #available(iOS 16.0, *),
let textLayoutManager,
let contentManager = textLayoutManager.textContentManager {
// convert from NSRange to NSTextRange
// i have no idea under what circumstances any of these calls could fail

View File

@ -1,5 +1,5 @@
//
// CopyableLabel.swift
// CopyableLable.swift
// Tusker
//
// Created by Shadowfacts on 2/4/23.
@ -8,7 +8,7 @@
import UIKit
class CopyableLabel: UILabel {
class CopyableLable: UILabel {
private var _editMenuInteraction: Any!
@available(iOS 16.0, *)
@ -28,11 +28,13 @@ class CopyableLabel: UILabel {
}
private func commonInit() {
if #available(iOS 16.0, *) {
editMenuInteraction = UIEditMenuInteraction(delegate: nil)
addInteraction(editMenuInteraction)
isUserInteractionEnabled = true
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressed)))
}
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = text

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" 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="23084"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -124,16 +125,16 @@
</constraints>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
<rect key="frame" x="144" y="235" width="101.5" height="23"/>
<rect key="frame" x="144" y="235" width="103.5" height="23"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL" customClass="CopyableLabel" customModule="Tusker" customModuleProvider="target">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL" customClass="CopyableLable" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="2.5" width="81" height="18"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" image="lock.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KNY-GD-beC">
<rect key="frame" x="85" y="1.5" width="16.5" height="19.5"/>
<rect key="frame" x="85" y="1.5" width="18.5" height="19.5"/>
<color key="tintColor" systemColor="secondaryLabelColor"/>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="light"/>
</imageView>
@ -186,14 +187,14 @@
</view>
</objects>
<resources>
<image name="ellipsis" catalog="system" width="32" height="32"/>
<image name="lock.fill" catalog="system" width="32" height="32"/>
<image name="person.badge.plus" catalog="system" width="32" height="32"/>
<image name="ellipsis" catalog="system" width="128" height="37"/>
<image name="lock.fill" catalog="system" width="125" height="128"/>
<image name="person.badge.plus" catalog="system" width="128" height="124"/>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

@ -143,7 +143,10 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
$0.isEditable = false
$0.isSelectable = true
$0.emojiFont = ConversationMainStatusCollectionViewCell.contentFont
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber, .money, .physicalValue]
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
if #available(iOS 16.0, *) {
$0.dataDetectorTypes.formUnion([.money, .physicalValue])
}
}
private var translateButton: TranslateButton?