I would like my screen to show a scrolling list and a button below it. The button should always be visible. I tried the following, but it did does not work. The LazyColumn takes the entire screen. I want the Button to take it's space at the bottom, and then the LazyColumn has the rest of the screen.
Column(Modifier.fillMaxSize()) {
LazyColumn {
items(50) { i ->
Text("Row $i", Modifier.fillMaxWidth().padding(8.dp))
}
}
Button(onClick = { println("hi") }) {
Text("Hello")
}
}
You can apply the weight modifier to the lazyColumn:
Column(Modifier.fillMaxSize()) {
LazyColumn(Modifier.weight(1f)) {
items(50) { i ->
Text("Row $i", Modifier.fillMaxWidth().padding(8.dp))
}
}
Button(onClick = { println("hi") }) {
Text("Hello")
}
}
This is also explained in the Jetpack Compose basics codelab
Related
I have text in a scroll-view that I would like to add a context menu on. I also want to close the keyboard when the context menu is open.
Here is some sample code:
struct TestView: View {
var body: some View {
let texts = Array(1...100).map { "Test \($0)" }
ScrollView {
VStack(spacing: 10) {
ForEach(texts, id: \.self) { text in
Text(text)
.frame(maxWidth: .infinity)
.contextMenu {
Button {
UIPasteboard.general.string = text
} label: {
Label("Copy", systemImage: "doc.on.clipboard")
}
}
.simultaneousGesture(LongPressGesture(minimumDuration: 0.5).onEnded { _ in
print("Do something else when the context menu is opened. (i.e. close keyboard)")
})
}
}
}
}
}
This code prevents me from being able to scroll on the scrollview starting from the text though.
I've seen in other answers that adding onTapGesture before the other gestures can fix it, but it doesn't seem to work in this case.
I want to achieve behavior close to native iOS reminders app on iPad.
There is dropdown Menu in NavigationBar that shows tags in it. I thought that I can do it with Picker but as I see there is now multiple selection support in it.
I did it using
...
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
MenuView()
} label: {
Image(systemName: "number")
}
Button("Done") {}
}
}
And MenuView's code is
struct MenuView: View {
#State var tags = ["#tag1", "#tag2", "#tag3", "#tag4"]
var body: some View {
ForEach(tags, id:\.self) { tag in
Button {
} label: {
if selectedTags.contains(tag) {
Label(tag, systemImage: "checkmark")
} else {
Text(tag)
}
}
}
Section {
Button {
} label: {
Label("Configure", systemImage: "ellipsis")
}
}
}
}
The main problem right now is when I press any of this buttons, drop down is closed, while I want this button to only toggle selected status.
I tried to use Text instead of buttons but didn't find way to handle tap gestures from them, they look like being disabled by some view modifier.
What is the optimal way to handle it?
I am trying to inset the bottom of a list by the height of the keyboard when the keyboard shows and the list is scrolled to the bottom. I know this question has been asked in different scenarios but I haven't found any proper solution yet. This is specifically for a chat app and the simple code below demonstrates the problem:
#State var text: String = ""
#FocusState private var keyboardVisible: Bool
var body: some View {
NavigationView {
VStack {
List {
ForEach(1...100, id: \.self) { item in
Text("\(item)")
}
}
.navigationTitle("Conversations")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
keyboardVisible = false
} label: {
Text("Hide Keyboard")
}
}
}
ZStack {
Color.red
.frame(height: 44)
TextEditor(text: $text)
.frame(height: 32)
.focused($keyboardVisible)
}
}
}
}
If you scroll to the end and tap in the textEditor, the text editor moves up with the keyboard as expected, but the list doesn't move the content up. I wonder how I can achieve this and have it move up smoothly with the keyboard.
You don't mention trying it, but this is exactly what we have a ScrollViewReader() for. There is a bit of an issue using it in this case, that can be worked around. The issue is that keyboardVisible changes BEFORE the keyboard is fully up. If you scroll at that point, you will cut off the bottom of the List entries. So, we need to delay the reader with DispatchQueue.main.asyncAfter(deadline:). This causes enough delay that when the reader actually reads the position, the keyboard is at full height, and the scroll to the bottom does its job.
#State var text: String = ""
#FocusState private var keyboardVisible: Bool
var body: some View {
NavigationView {
VStack {
ScrollViewReader { scroll in
List {
ForEach(1...100, id: \.self) { item in
Text("\(item)")
}
}
.onChange(of: keyboardVisible, perform: { _ in
if keyboardVisible {
withAnimation(.easeIn(duration: 1)) {
scroll.scrollTo(100) // this would be your array.count - 1,
// but you hard coded your ForEach
}
// The scroll has to wait until the keyboard is fully up
// this causes it to wait just a bit.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// this helps make it look deliberate and finished
withAnimation(.easeInOut(duration: 1)) {
scroll.scrollTo(100) // this would be your array.count - 1,
// but you hard coded your ForEach
}
}
}
})
.navigationTitle("Conversations")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
keyboardVisible = false
} label: {
Text("Hide Keyboard")
}
}
}
ZStack {
Color.red
.frame(height: 44)
TextEditor(text: $text)
.frame(height: 32)
.focused($keyboardVisible)
}
}
}
}
}
Edit:
I changed the code to scroll twice. The first starts the scroll immediately, and the second scrolls it after the keyboard is up to finish the job. It starts quickly and ends smoothly. I also left the comments in the code for the next person; you don't need them.
I can't seem to find how to bring a view to the center like a popup and blur background like when you long press a message in the messages app.
I know there is a simple native way to do this but I can't seem to find out how.
You are looking for contextMenu(menuItems:). It shows those buttons you see, and automatically blurs the background content to focus the selected view.
Example:
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 30) {
ForEach(0 ..< 20) { _ in
Text(String(Int.random(in: 1 ... 100000000)))
.padding()
.contextMenu {
Button {
//
} label: {
Label("Reply", systemImage: "arrowshape.turn.up.left")
}
Button {
//
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
Button {
//
} label: {
Label("Translate", systemImage: "arrow.left.arrow.right")
}
Button {
//
} label: {
Label("Moreā¦", systemImage: "ellipsis.circle")
}
}
}
}
}
}
}
Result:
Say I have a List and two buttons in one row, how can I distinguish which button is tapped without the entire row highlighting?
For this sample code, when any one of the buttons in the row is tapped, both button's action callbacks are invoked.
// a simple list with just one row
List {
// both buttons in a HStack so that they appear in a single row
HStack {
Button {
print("button 1 tapped")
} label: {
Text("One")
}
Button {
print("button 2 tapped")
} label: {
Text("Two")
}
}
}
When only one of buttons is tapped once, I see the callbacks for both buttons being called, which is not what I want:
button 1 tapped
button 2 tapped
You need to use BorderlessButtonStyle() or PlainButtonStyle().
List([1, 2, 3], id: \.self) { row in
HStack {
Button(action: { print("Button at \(row)") }) {
Text("Row: \(row) Name: A")
}
.buttonStyle(BorderlessButtonStyle())
Button(action: { print("Button at \(row)") }) {
Text("Row: \(row) Name: B")
}
.buttonStyle(PlainButtonStyle())
}
}
Seems to be a specific issue concerning Button when contained in a List row.
Workaround:
List {
HStack {
Text("One").onTapGesture { print("One") }
Text("Two").onTapGesture { print("Two") }
}
}
This yields the desired output.
You can also use a Group instead of Text to have a sophisticated design for the "buttons".
One of the differences with SwiftUI is that you are not creating specific instances of, for example UIButton, because you might be in a Mac app. With SwiftUI, you are requesting a button type thing.
In this case since you are in a list row, the system gives you a full size, tap anywhere to trigger the action, button. And since you've added two of them, both are triggered when you tap anywhere.
You can add two separate Views and give them a .onTapGesture to have them act essentially as buttons, but you would lose the tap flash of the cell row and any other automatic button like features SwiftUI would give.
List {
HStack {
Text("One").onTapGesture {
print("Button 1 tapped")
}
Spacer()
Text("Two").onTapGesture {
print("Button 2 tapped")
}
}
}
You need to create your own ButtonStyle:
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(.accentColor)
.opacity(configuration.isPressed ? 0.5 : 1.0)
}
}
struct IdentifiableString: Identifiable {
let text: String
var id: String { text }
}
struct Test: View {
var body: some View {
List([
IdentifiableString(text: "Line 1"),
IdentifiableString(text: "Line 2"),
]) {
item in
HStack {
Text("\(item.text)")
Spacer()
Button(action: { print("\(item.text) 1")}) {
Text("Button 1")
}
Button(action: { print("\(item.text) 2")}) {
Text("Button 2")
}
}
}.buttonStyle(MyButtonStyle())
}
}