Refactor ComposeView to use a single List for everything

This commit is contained in:
Shadowfacts 2022-11-07 22:55:39 -05:00
parent 5a5c67e445
commit 7600954f4b
10 changed files with 143 additions and 97 deletions

View File

@ -152,6 +152,7 @@
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; }; D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; }; D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; }; D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */; };
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; }; D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284724ECBCB100C732D3 /* ComposeView.swift */; }; D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284724ECBCB100C732D3 /* ComposeView.swift */; };
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */; }; D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */; };
@ -507,6 +508,7 @@
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; }; D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; }; D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; }; D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPicker.swift; sourceTree = "<group>"; };
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; }; D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; };
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; }; D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCurrentAccount.swift; sourceTree = "<group>"; }; D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCurrentAccount.swift; sourceTree = "<group>"; };
@ -1289,6 +1291,7 @@
D620483323D3801D008A63EF /* LinkTextView.swift */, D620483323D3801D008A63EF /* LinkTextView.swift */,
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */, D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */,
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */, D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */,
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */,
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */, D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */, D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */, D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
@ -1853,6 +1856,7 @@
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */, D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */, D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */, D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */, D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */, D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,

View File

@ -14,7 +14,6 @@ import Vision
struct ComposeAttachmentRow: View { struct ComposeAttachmentRow: View {
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
@ObservedObject var attachment: CompositionAttachment @ObservedObject var attachment: CompositionAttachment
let heightChanged: (CGFloat) -> Void
@EnvironmentObject var uiState: ComposeUIState @EnvironmentObject var uiState: ComposeUIState
@State private var mode: Mode = .allowEntry @State private var mode: Mode = .allowEntry
@ -47,7 +46,6 @@ struct ComposeAttachmentRow: View {
switch mode { switch mode {
case .allowEntry: case .allowEntry:
ComposeTextView(text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80) ComposeTextView(text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80)
.heightDidChange(self.heightChanged)
.backgroundColor(.clear) .backgroundColor(.clear)
case .recognizingText: case .recognizingText:

View File

@ -18,23 +18,17 @@ struct ComposeAttachmentsList: View {
@EnvironmentObject var uiState: ComposeUIState @EnvironmentObject var uiState: ComposeUIState
@State var isShowingAssetPickerPopover = false @State var isShowingAssetPickerPopover = false
@State var isShowingCreateDrawing = false @State var isShowingCreateDrawing = false
@State var rowHeights = [UUID: CGFloat]()
@Environment(\.colorScheme) var colorScheme: ColorScheme @Environment(\.colorScheme) var colorScheme: ColorScheme
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var body: some View { var body: some View {
List { Group {
ForEach(draft.attachments) { (attachment) in ForEach(draft.attachments) { (attachment) in
ComposeAttachmentRow( ComposeAttachmentRow(
draft: draft, draft: draft,
attachment: attachment attachment: attachment
) { (newHeight) in )
// in case height changed callback is called after atachment is removed but before view hierarchy is updated
if draft.attachments.contains(where: { $0.id == attachment.id }) {
rowHeights[attachment.id] = newHeight
}
}
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) .listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
.onDrag { NSItemProvider(object: attachment) } .onDrag { NSItemProvider(object: attachment) }
} }
@ -69,12 +63,7 @@ struct ComposeAttachmentsList: View {
.frame(height: cellHeight / 2) .frame(height: cellHeight / 2)
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) .listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
} }
.listStyle(PlainListStyle())
// todo: scrollDisabled doesn't remove the need for manually calculating the frame height
.frame(height: totalListHeight)
.scrollDisabledIfAvailable(totalHeight: totalListHeight)
.onAppear(perform: self.didAppear) .onAppear(perform: self.didAppear)
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
} }
private var addButtonImageName: String { private var addButtonImageName: String {
@ -104,13 +93,6 @@ struct ComposeAttachmentsList: View {
} }
} }
private var totalListHeight: CGFloat {
let totalRowHeights = rowHeights.values.reduce(0, +)
let totalPadding = CGFloat(draft.attachments.count) * cellPadding
let addButtonHeight = 3 * (cellHeight / 2 + cellPadding)
return totalRowHeights + totalPadding + addButtonHeight
}
private func didAppear() { private func didAppear() {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
// these appearance proxy hacks are no longer necessary // these appearance proxy hacks are no longer necessary
@ -122,17 +104,6 @@ struct ComposeAttachmentsList: View {
} }
} }
private func attachmentsChanged(attachments: [CompositionAttachment]) {
var copy = rowHeights
for k in copy.keys where !attachments.contains(where: { k == $0.id }) {
copy.removeValue(forKey: k)
}
for attachment in attachments where !copy.keys.contains(attachment.id) {
copy[attachment.id] = cellHeight
}
self.rowHeights = copy
}
private func assetPickerPopover() -> some View { private func assetPickerPopover() -> some View {
ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate) ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate)
.onDisappear { .onDisappear {
@ -214,16 +185,6 @@ fileprivate extension View {
self self
} }
} }
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDisabledIfAvailable(totalHeight: CGFloat) -> some View {
if #available(iOS 16.0, *) {
self.scrollDisabled(true)
} else {
self.frame(height: totalHeight)
}
}
} }
@available(iOS 16.0, *) @available(iOS 16.0, *)

View File

@ -429,6 +429,10 @@ extension ComposeHostingController: UIAdaptivePresentationControllerDelegate {
return Preferences.shared.automaticallySaveDrafts || !draft.hasContent return Preferences.shared.automaticallySaveDrafts || !draft.hasContent
} }
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: self, for: nil)
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
uiState.isShowingSaveDraftSheet = true uiState.isShowingSaveDraftSheet = true
} }

View File

@ -44,6 +44,7 @@ struct ComposePollView: View {
.imageScale(.small) .imageScale(.small)
.padding(4) .padding(4)
} }
.buttonStyle(.plain)
.accentColor(buttonForegroundColor) .accentColor(buttonForegroundColor)
.background(Circle().foregroundColor(buttonBackgroundColor)) .background(Circle().foregroundColor(buttonBackgroundColor))
.hoverEffect() .hoverEffect()
@ -52,31 +53,22 @@ struct ComposePollView: View {
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
ComposePollOption(poll: poll, option: e.element, optionIndex: e.offset) ComposePollOption(poll: poll, option: e.element, optionIndex: e.offset)
} }
.transition(.slide)
Button(action: self.addOption) { Button(action: self.addOption) {
Label("Add Option", systemImage: "plus") Label("Add Option", systemImage: "plus")
} }
.buttonStyle(.borderless)
HStack { HStack {
// use .animation(nil) on pickers so frame doesn't have a size change animation when the text changes MenuPicker(selection: $poll.multiple, options: [
// this is deprecated in iOS 15, but using .animation(nil, value: poll.multiple) does not work (it still animates) .init(title: "Allow multiple", value: true),
// nor does setting that on the Text rather than the Picker .init(title: "Single choice", value: false),
Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choice" : "Single choice")) { ])
Text("Allow multiple choices").tag(true)
Text("Single choice").tag(false)
}
.animation(nil)
.pickerStyle(MenuPickerStyle())
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
Picker(selection: $duration, label: Text(verbatim: ComposePollView.formatter.string(from: duration.timeInterval)!)) { MenuPicker(selection: $duration, options: Duration.allCases.map {
ForEach(Duration.allCases, id: \.self) { (duration) in .init(title: ComposePollView.formatter.string(from: $0.timeInterval)!, value: $0)
Text(ComposePollView.formatter.string(from: duration.timeInterval)!).tag(duration) })
}
}
.animation(nil)
.pickerStyle(MenuPickerStyle())
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
@ -110,9 +102,7 @@ struct ComposePollView: View {
} }
private func addOption() { private func addOption() {
withAnimation(.easeInOut(duration: 0.25)) { poll.options.append(Draft.Poll.Option(""))
poll.options.append(Draft.Poll.Option(""))
}
} }
} }
@ -163,6 +153,7 @@ struct ComposePollOption: View {
Button(action: self.removeOption) { Button(action: self.removeOption) {
Image(systemName: "minus.circle.fill") Image(systemName: "minus.circle.fill")
} }
.buttonStyle(.plain)
.foregroundColor(poll.options.count == 1 ? .gray : .red) .foregroundColor(poll.options.count == 1 ? .gray : .red)
.disabled(poll.options.count == 1) .disabled(poll.options.count == 1)
.hoverEffect() .hoverEffect()
@ -175,9 +166,7 @@ struct ComposePollOption: View {
} }
private func removeOption() { private func removeOption() {
_ = withAnimation { poll.options.remove(at: optionIndex)
poll.options.remove(at: optionIndex)
}
} }
struct Checkbox: View { struct Checkbox: View {

View File

@ -17,18 +17,20 @@ struct ComposeReplyContentView: UIViewRepresentable {
let heightChanged: (CGFloat) -> Void let heightChanged: (CGFloat) -> Void
func makeUIView(context: Context) -> ComposeReplyContentTextView { func makeUIView(context: Context) -> UIViewType {
let view = ComposeReplyContentTextView() let view = ComposeReplyContentTextView()
view.overrideMastodonController = mastodonController view.overrideMastodonController = mastodonController
view.setTextFrom(status: status) view.setTextFrom(status: status)
view.isUserInteractionEnabled = false view.isUserInteractionEnabled = false
// scroll needs to be enabled, otherwise the text view never reports a contentSize greater than 1 line
view.isScrollEnabled = true
view.backgroundColor = .clear view.backgroundColor = .clear
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return view return view
} }
func updateUIView(_ uiView: ComposeReplyContentTextView, context: Context) { func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.heightChanged = heightChanged uiView.heightChanged = heightChanged
} }
} }

View File

@ -10,7 +10,8 @@ import SwiftUI
struct ComposeReplyView: View { struct ComposeReplyView: View {
let status: StatusMO let status: StatusMO
let stackPadding: CGFloat let rowTopInset: CGFloat
let globalFrameOutsideList: CGRect
@State private var displayNameHeight: CGFloat? @State private var displayNameHeight: CGFloat?
@State private var contentHeight: CGFloat? @State private var contentHeight: CGFloat?
@ -46,7 +47,11 @@ struct ComposeReplyView: View {
}) })
ComposeReplyContentView(status: status) { newHeight in ComposeReplyContentView(status: status) { newHeight in
contentHeight = newHeight // otherwise, with long in-reply-to statuses, the main content text view position seems not to update
// and it ends up partially behind the header
DispatchQueue.main.async {
contentHeight = newHeight
}
} }
.frame(height: contentHeight ?? 0) .frame(height: contentHeight ?? 0)
} }
@ -55,10 +60,12 @@ struct ComposeReplyView: View {
} }
private func replyAvatarImage(geometry: GeometryProxy) -> some View { private func replyAvatarImage(geometry: GeometryProxy) -> some View {
let scrollOffset = -geometry.frame(in: .named(ComposeView.coordinateSpaceOutsideOfScrollView)).minY // using a coordinate space declared outside of the List doesn't work, so we do the math ourselves
let globalFrame = geometry.frame(in: .global)
let scrollOffset = -(globalFrame.minY - globalFrameOutsideList.minY)
// add stackPadding so that the image is always at least stackPadding away from the top // add rowTopInset so that the image is always at least rowTopInset away from the top
var offset = scrollOffset + stackPadding var offset = scrollOffset + rowTopInset
// offset can never be less than 0 (i.e., above the top of the in-reply-to content) // offset can never be less than 0 (i.e., above the top of the in-reply-to content)
offset = max(offset, 0) offset = max(offset, 0)

View File

@ -42,12 +42,12 @@ import Combine
} }
struct ComposeView: View { struct ComposeView: View {
static let coordinateSpaceOutsideOfScrollView = "coordinateSpaceOutsideOfScrollView"
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
@EnvironmentObject var mastodonController: MastodonController @EnvironmentObject var mastodonController: MastodonController
@EnvironmentObject var uiState: ComposeUIState @EnvironmentObject var uiState: ComposeUIState
@State private var globalFrameOutsideList: CGRect = .zero
@OptionalStateObject private var poster: PostService? @OptionalStateObject private var poster: PostService?
@State private var isShowingPostErrorAlert = false @State private var isShowingPostErrorAlert = false
@State private var postError: PostService.Error? @State private var postError: PostService.Error?
@ -80,11 +80,8 @@ struct ComposeView: View {
var body: some View { var body: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
ScrollView(.vertical) { mainList
mainStack .scrollDismissesKeyboardInteractivelyIfAvailable()
}
.coordinateSpace(name: ComposeView.coordinateSpaceOutsideOfScrollView)
.scrollDismissesKeyboardInteractivelyIfAvailable()
if let poster = poster { if let poster = poster {
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149 // can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
@ -93,6 +90,13 @@ struct ComposeView: View {
autocompleteSuggestions autocompleteSuggestions
} }
.background(GeometryReader { proxy in
Color.clear
.preference(key: GlobalFrameOutsideListPrefKey.self, value: proxy.frame(in: .global))
.onPreferenceChange(GlobalFrameOutsideListPrefKey.self) { frame in
globalFrameOutsideList = frame
}
})
.navigationBarTitle("Compose") .navigationBarTitle("Compose")
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet) .actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
.alert(isPresented: $isShowingPostErrorAlert) { .alert(isPresented: $isShowingPostErrorAlert) {
@ -120,41 +124,49 @@ struct ComposeView: View {
.animation(.default, value: uiState.autocompleteState) .animation(.default, value: uiState.autocompleteState)
} }
var mainStack: some View { var mainList: some View {
VStack(alignment: .leading, spacing: 8) { List {
if let id = draft.inReplyToID, if let id = draft.inReplyToID,
let status = mastodonController.persistentContainer.status(for: id) { let status = mastodonController.persistentContainer.status(for: id) {
ComposeReplyView( ComposeReplyView(
status: status, status: status,
stackPadding: stackPadding rowTopInset: 8,
globalFrameOutsideList: globalFrameOutsideList
) )
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
} }
header header
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
if draft.contentWarningEnabled { if draft.contentWarningEnabled {
ComposeEmojiTextField(text: $draft.contentWarning, placeholder: "Write your warning here") ComposeEmojiTextField(text: $draft.contentWarning, placeholder: "Write your warning here")
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
} }
MainComposeTextView( MainComposeTextView(draft: draft)
draft: draft .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
) .listRowSeparator(.hidden)
if let poll = draft.poll { if let poll = draft.poll {
ComposePollView(draft: draft, poll: poll) ComposePollView(draft: draft, poll: poll)
.transition(.opacity.combined(with: .asymmetric(insertion: .scale(scale: 0.5, anchor: .leading), removal: .scale(scale: 0.5, anchor: .trailing)))) .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
} }
ComposeAttachmentsList( ComposeAttachmentsList(
draft: draft draft: draft
) )
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack .listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
.padding([.top, .bottom], -8)
} }
.animation(.default, value: draft.poll?.options.count)
.scrollDismissesKeyboardInteractivelyIfAvailable()
.listStyle(.plain)
.disabled(isPosting) .disabled(isPosting)
.padding(stackPadding) .padding(.bottom, uiState.autocompleteState != nil ? 46 : 0)
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
} }
private var header: some View { private var header: some View {
@ -258,6 +270,13 @@ private extension View {
} }
} }
private struct GlobalFrameOutsideListPrefKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
//struct ComposeView_Previews: PreviewProvider { //struct ComposeView_Previews: PreviewProvider {
// static var previews: some View { // static var previews: some View {
// ComposeView() // ComposeView()

View File

@ -17,6 +17,10 @@ struct MainComposeTextView: View {
if Date().formatted(date: .numeric, time: .omitted).starts(with: "3") { if Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
return Text("Happy π day!") return Text("Happy π day!")
} }
} else if components.month == 9 && components.day == 5 {
// https://weirder.earth/@noracodes/109276419847254552
// https://retrocomputing.stackexchange.com/questions/14763/what-warning-was-given-on-attempting-to-post-to-usenet-circa-1990
return Text("This program posts news to thousands of machines throughout the entire populated world. Please be sure you know what you are doing.").italic()
} else if components.month == 9 && components.day == 21 { } else if components.month == 9 && components.day == 21 {
return Text("Do you remember?") return Text("Do you remember?")
} else if components.month == 10 && components.day == 31 { } else if components.month == 10 && components.day == 31 {

View File

@ -0,0 +1,58 @@
//
// MenuPicker.swift
// Tusker
//
// Created by Shadowfacts on 11/7/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import SwiftUI
struct MenuPicker<Value: Hashable>: View {
@Binding var selection: Value
let options: [Option]
private var selectedOption: Option {
options.first(where: { $0.value == selection })!
}
var body: some View {
Menu {
ForEach(options, id: \.value) { option in
Button {
selection = option.value
} label: {
Label(option.title, systemImage: selection == option.value ? "checkmark" : "")
}
}
} label: {
// zstack so that the size of the picker is the size of the largest option
ZStack {
ForEach(options, id: \.value) { option in
HStack {
Text(option.title)
Image(systemName: "chevron.up.chevron.down")
}
.opacity(option.value == selection ? 1 : 0)
}
}
}
.menuStyle(.borderlessButton)
}
struct Option {
let title: String
let value: Value
}
}
struct MenuPicker_Previews: PreviewProvider {
@State static var value = 0
static var previews: some View {
MenuPicker(selection: $value, options: [
.init(title: "Zero", value: 0),
.init(title: "One", value: 1),
.init(title: "Two", value: 2),
])
}
}