Remove pre-iOS 16 code
This commit is contained in:
parent
8243e06e95
commit
cad074bcc3
|
@ -30,20 +30,14 @@ enum ToolbarElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct FocusedComposeInput: FocusedValueKey {
|
private struct FocusedComposeInput: FocusedValueKey {
|
||||||
typealias Value = (any ComposeInput)?
|
typealias Value = any ComposeInput
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FocusedValues {
|
extension FocusedValues {
|
||||||
// This double optional is unfortunate, but avoiding it requires iOS 16 API
|
var composeInput: (any ComposeInput)? {
|
||||||
fileprivate var _composeInput: (any ComposeInput)?? {
|
|
||||||
get { self[FocusedComposeInput.self] }
|
get { self[FocusedComposeInput.self] }
|
||||||
set { self[FocusedComposeInput.self] = newValue }
|
set { self[FocusedComposeInput.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var composeInput: (any ComposeInput)? {
|
|
||||||
get { _composeInput ?? nil }
|
|
||||||
set { _composeInput = newValue }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
|
@ -72,6 +66,6 @@ struct FocusedInputModifier: ViewModifier {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.environment(\.composeInputBox, box)
|
.environment(\.composeInputBox, box)
|
||||||
.focusedValue(\._composeInput, box.wrappedValue)
|
.focusedValue(\.composeInput, box.wrappedValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,93 +28,12 @@ struct AttachmentsListView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
WrappedCollectionView(attachments: draft.draftAttachments, hasPoll: draft.poll != nil, callbacks: callbacks, canAddAttachment: canAddAttachment)
|
WrappedCollectionView(attachments: draft.draftAttachments, hasPoll: draft.poll != nil, callbacks: callbacks, canAddAttachment: canAddAttachment)
|
||||||
// Impose a minimum height, because otherwise it defaults to zero which prevents the collection
|
// Impose a minimum height, because otherwise it defaults to zero which prevents the collection
|
||||||
// view from laying out, and leaving the intrinsic content size at zero too.
|
// view from laying out, and leaving the intrinsic content size at zero too.
|
||||||
.frame(minHeight: 50)
|
.frame(minHeight: 50)
|
||||||
.padding(.horizontal, -8)
|
.padding(.horizontal, -8)
|
||||||
} else {
|
|
||||||
LegacyAttachmentsList(draft: draft, callbacks: callbacks, canAddAttachment: canAddAttachment)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private struct LegacyAttachmentsList: View {
|
|
||||||
@ObservedObject var draft: Draft
|
|
||||||
let callbacks: Callbacks
|
|
||||||
let canAddAttachment: Bool
|
|
||||||
@State private var attachmentHeights = [NSManagedObjectID: CGFloat]()
|
|
||||||
|
|
||||||
private var totalHeight: CGFloat {
|
|
||||||
let buttonsHeight = 3 * (40 + AttachmentsListPaddingModifier.cellPadding)
|
|
||||||
let rowHeights = draft.attachments.compactMap {
|
|
||||||
attachmentHeights[($0 as! NSManagedObject).objectID]
|
|
||||||
}.reduce(0) { partialResult, height in
|
|
||||||
partialResult + height + AttachmentsListPaddingModifier.cellPadding
|
|
||||||
}
|
|
||||||
return buttonsHeight + rowHeights
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
.listStyle(.plain)
|
|
||||||
.frame(height: totalHeight)
|
|
||||||
.scrollDisabledIfAvailable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var content: some View {
|
|
||||||
ForEach(draft.draftAttachments) { attachment in
|
|
||||||
AttachmentRowView(attachment: attachment)
|
|
||||||
.modifier(AttachmentRowHeightModifier(height: $attachmentHeights[attachment.objectID]))
|
|
||||||
}
|
|
||||||
.onMove(perform: self.moveAttachments)
|
|
||||||
.onDelete(perform: self.removeAttachments)
|
|
||||||
|
|
||||||
AddPhotoButton(canAddAttachment: canAddAttachment, draft: draft, insertAttachments: self.insertAttachments)
|
|
||||||
|
|
||||||
AddDrawingButton(canAddAttachment: canAddAttachment)
|
|
||||||
|
|
||||||
TogglePollButton(poll: draft.poll)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move this to Callbacks
|
|
||||||
private func insertAttachments(at offset: Int, itemProviders: [NSItemProvider]) {
|
|
||||||
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
|
|
||||||
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
|
|
||||||
guard let attachment = object as? DraftAttachment else { return }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard self.canAddAttachment else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DraftsPersistentContainer.shared.viewContext.insert(attachment)
|
|
||||||
attachment.draft = self.draft
|
|
||||||
self.draft.attachments.add(attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move this to Callbacks
|
|
||||||
private func moveAttachments(from source: IndexSet, to destination: Int) {
|
|
||||||
// just using moveObjects(at:to:) on the draft.attachments NSMutableOrderedSet
|
|
||||||
// results in the order switching back to the previous order and then to the correct one
|
|
||||||
// on the subsequent 2 view updates. creating a new set with the proper order doesn't have that problem
|
|
||||||
var array = draft.draftAttachments
|
|
||||||
array.move(fromOffsets: source, toOffset: destination)
|
|
||||||
draft.attachments = NSMutableOrderedSet(array: array)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeAttachments(at indices: IndexSet) {
|
|
||||||
for index in indices {
|
|
||||||
callbacks.removeAttachment(at: index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct Callbacks {
|
private struct Callbacks {
|
||||||
|
@ -157,104 +76,6 @@ private struct Callbacks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AttachmentRowHeightPreferenceKey: PreferenceKey {
|
|
||||||
static var defaultValue: CGFloat { 0 }
|
|
||||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
||||||
value = nextValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AttachmentRowHeightModifier: ViewModifier {
|
|
||||||
@Binding var height: CGFloat?
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.background {
|
|
||||||
GeometryReader { proxy in
|
|
||||||
Color.clear
|
|
||||||
// do the preference dance because onChange(of:inital:_:) is iOS 17+ :/
|
|
||||||
.preference(key: AttachmentRowHeightPreferenceKey.self, value: proxy.size.height)
|
|
||||||
.onPreferenceChange(AttachmentRowHeightPreferenceKey.self) { newValue in
|
|
||||||
height = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AttachmentsListPaddingModifier: ViewModifier {
|
|
||||||
static let cellPadding: CGFloat = 12
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.listRowInsets(EdgeInsets(top: Self.cellPadding / 2, leading: Self.cellPadding / 2, bottom: Self.cellPadding / 2, trailing: Self.cellPadding / 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AttachmentsListButton<Label: View>: View {
|
|
||||||
let action: () -> Void
|
|
||||||
@ViewBuilder let label: Label
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
label
|
|
||||||
}
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
.frame(height: 40)
|
|
||||||
.modifier(AttachmentsListPaddingModifier())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AddPhotoButton: View {
|
|
||||||
let canAddAttachment: Bool
|
|
||||||
let draft: Draft
|
|
||||||
let insertAttachments: (Int, [NSItemProvider]) -> Void
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
|
||||||
@Environment(\.composeUIConfig.presentAssetPicker) private var presentAssetPicker
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
AttachmentsListButton {
|
|
||||||
presentAssetPicker?() { results in
|
|
||||||
insertAttachments(draft.attachments.count, results.map(\.itemProvider))
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label("Add photo or video", systemImage: colorScheme == .dark ? "photo.fill" : "photo")
|
|
||||||
}
|
|
||||||
.disabled(!canAddAttachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AddDrawingButton: View {
|
|
||||||
let canAddAttachment: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
AttachmentsListButton {
|
|
||||||
fatalError("TODO")
|
|
||||||
} label: {
|
|
||||||
Label("Draw something", systemImage: "hand.draw")
|
|
||||||
}
|
|
||||||
.disabled(!canAddAttachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TogglePollButton: View {
|
|
||||||
let poll: Poll?
|
|
||||||
|
|
||||||
var canAddPoll: Bool {
|
|
||||||
// TODO
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
AttachmentsListButton {
|
|
||||||
fatalError("TODO")
|
|
||||||
} label: {
|
|
||||||
Label(poll == nil ? "Add a poll" : "Remove poll", systemImage: "chart.bar.doc.horizontal")
|
|
||||||
}
|
|
||||||
.disabled(!canAddPoll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
private struct WrappedCollectionView: UIViewRepresentable {
|
private struct WrappedCollectionView: UIViewRepresentable {
|
||||||
let attachments: [DraftAttachment]
|
let attachments: [DraftAttachment]
|
||||||
|
|
|
@ -41,7 +41,7 @@ struct ComposeToolbarView: View {
|
||||||
|
|
||||||
VisibilityButton(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
VisibilityButton(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
||||||
|
|
||||||
LocalOnlyButton(enabled: $draft.contentWarningEnabled, mastodonController: mastodonController)
|
LocalOnlyButton(enabled: $draft.localOnly, mastodonController: mastodonController)
|
||||||
|
|
||||||
InsertEmojiButton()
|
InsertEmojiButton()
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ private struct ToolbarScrollView<Content: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
|
.scrollDisabled(realWidth ?? 0 <= minWidth ?? 0)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background {
|
.background {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
|
@ -246,8 +246,7 @@ private struct LangaugeButton: View {
|
||||||
@State private var hasChanged = false
|
@State private var hasChanged = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *),
|
if instanceFeatures.createStatusWithLanguage {
|
||||||
instanceFeatures.createStatusWithLanguage {
|
|
||||||
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $hasChanged)
|
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $hasChanged)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification), perform: currentInputModeChanged)
|
.onReceive(NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification), perform: currentInputModeChanged)
|
||||||
.onChange(of: draft.id) { _ in
|
.onChange(of: draft.id) { _ in
|
||||||
|
|
|
@ -15,16 +15,9 @@ struct ComposeView: View {
|
||||||
@EnvironmentObject private var controller: ComposeController
|
@EnvironmentObject private var controller: ComposeController
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
navigationRoot
|
navigationRoot
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
NavigationView {
|
|
||||||
navigationRoot
|
|
||||||
}
|
|
||||||
.navigationViewStyle(.stack)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var navigationRoot: some View {
|
private var navigationRoot: some View {
|
||||||
|
@ -32,7 +25,7 @@ struct ComposeView: View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
scrollContent
|
scrollContent
|
||||||
}
|
}
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
.scrollDismissesKeyboard(.interactively)
|
||||||
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
||||||
.modifier(ToolbarSafeAreaInsetModifier())
|
.modifier(ToolbarSafeAreaInsetModifier())
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -18,7 +18,6 @@ struct NewMainTextView: View {
|
||||||
NewMainTextViewRepresentable(value: $value, becomeFirstResponder: $becomeFirstResponder)
|
NewMainTextViewRepresentable(value: $value, becomeFirstResponder: $becomeFirstResponder)
|
||||||
.focused($focusedField, equals: .body)
|
.focused($focusedField, equals: .body)
|
||||||
.modifier(FocusedInputModifier())
|
.modifier(FocusedInputModifier())
|
||||||
.modifier(HeightExpandingModifier(minHeight: Self.minHeight))
|
|
||||||
.overlay(alignment: .topLeading) {
|
.overlay(alignment: .topLeading) {
|
||||||
if value.isEmpty {
|
if value.isEmpty {
|
||||||
PlaceholderView()
|
PlaceholderView()
|
||||||
|
@ -37,18 +36,10 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||||
@Environment(\.composeUIConfig.useTwitterKeyboard) private var useTwitterKeyboard
|
@Environment(\.composeUIConfig.useTwitterKeyboard) private var useTwitterKeyboard
|
||||||
// TODO: test textSelectionStartsAtBeginning
|
// TODO: test textSelectionStartsAtBeginning
|
||||||
@Environment(\.composeUIConfig.textSelectionStartsAtBeginning) private var textSelectionStartsAtBeginning
|
@Environment(\.composeUIConfig.textSelectionStartsAtBeginning) private var textSelectionStartsAtBeginning
|
||||||
#if !os(visionOS)
|
|
||||||
@Environment(\.textViewContentHeight) @Binding private var textViewContentHeight
|
|
||||||
#endif
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UITextView {
|
func makeUIView(context: Context) -> UITextView {
|
||||||
let view: UITextView
|
|
||||||
// TODO: if we're not doing the pill background, reevaluate whether this version fork is necessary
|
// TODO: if we're not doing the pill background, reevaluate whether this version fork is necessary
|
||||||
if #available(iOS 16.0, *) {
|
let view = WrappedTextView(usingTextLayoutManager: true)
|
||||||
view = WrappedTextView(usingTextLayoutManager: true)
|
|
||||||
} else {
|
|
||||||
view = WrappedTextView()
|
|
||||||
}
|
|
||||||
view.delegate = context.coordinator
|
view.delegate = context.coordinator
|
||||||
view.adjustsFontForContentSizeCategory = true
|
view.adjustsFontForContentSizeCategory = true
|
||||||
view.textContainer.lineBreakMode = .byWordWrapping
|
view.textContainer.lineBreakMode = .byWordWrapping
|
||||||
|
@ -92,16 +83,6 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||||
becomeFirstResponder = false
|
becomeFirstResponder = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
if #unavailable(iOS 16.0) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let targetSize = CGSize(width: uiView.bounds.width, height: UIView.layoutFittingCompressedSize.height)
|
|
||||||
let fittingSize = uiView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultHigh)
|
|
||||||
textViewContentHeight = fittingSize.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeCoordinator() -> WrappedTextViewCoordinator {
|
func makeCoordinator() -> WrappedTextViewCoordinator {
|
||||||
|
@ -149,13 +130,8 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||||
let str = NSMutableAttributedString(string: text)
|
let str = NSMutableAttributedString(string: text)
|
||||||
let mentionMatches = CharacterCounter.mention.matches(in: text, range: NSRange(location: 0, length: str.length))
|
let mentionMatches = CharacterCounter.mention.matches(in: text, range: NSRange(location: 0, length: str.length))
|
||||||
for match in mentionMatches.reversed() {
|
for match in mentionMatches.reversed() {
|
||||||
let range: NSRange
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
||||||
range = NSRange(location: match.range.location, length: match.range.length + 1)
|
let range = NSRange(location: match.range.location, length: match.range.length + 1)
|
||||||
} else {
|
|
||||||
range = match.range
|
|
||||||
}
|
|
||||||
str.addAttributes([
|
str.addAttributes([
|
||||||
.mention: true,
|
.mention: true,
|
||||||
.foregroundColor: UIColor.tintColor,
|
.foregroundColor: UIColor.tintColor,
|
||||||
|
@ -199,7 +175,6 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||||
// the attribute range should always be one greater than the match range, to account for the text attachment
|
// the attribute range should always be one greater than the match range, to account for the text attachment
|
||||||
if attribute == nil || attributeRange.length <= match.range.length {
|
if attribute == nil || attributeRange.length <= match.range.length {
|
||||||
changed = true
|
changed = true
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
let newAttributeRange: NSRange
|
let newAttributeRange: NSRange
|
||||||
if attribute == nil {
|
if attribute == nil {
|
||||||
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
str.insert(NSAttributedString(attachment: Self.attachment), at: match.range.location)
|
||||||
|
@ -211,9 +186,6 @@ private final class WrappedTextViewCoordinator: NSObject {
|
||||||
.mention: true,
|
.mention: true,
|
||||||
.foregroundColor: UIColor.tintColor,
|
.foregroundColor: UIColor.tintColor,
|
||||||
], range: newAttributeRange)
|
], range: newAttributeRange)
|
||||||
} else {
|
|
||||||
str.addAttribute(.foregroundColor, value: UIColor.tintColor, range: match.range)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,38 +248,3 @@ private struct PlaceholderView: View {
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private struct HeightExpandingModifier: ViewModifier {
|
|
||||||
let minHeight: CGFloat
|
|
||||||
|
|
||||||
@State private var height: CGFloat?
|
|
||||||
private var effectiveHeight: CGFloat {
|
|
||||||
height.map { max($0, minHeight) } ?? minHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
content
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
.frame(height: effectiveHeight)
|
|
||||||
.environment(\.textViewContentHeight, $height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private struct TextViewContentHeightKey: EnvironmentKey {
|
|
||||||
static var defaultValue: Binding<CGFloat?> { .constant(nil) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
private extension EnvironmentValues {
|
|
||||||
var textViewContentHeight: Binding<CGFloat?> {
|
|
||||||
get { self[TextViewContentHeightKey.self] }
|
|
||||||
set { self[TextViewContentHeightKey.self] = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
Loading…
Reference in New Issue