So basically I am very new to SwiftUI(started a few days ago) and am trying to put tabs within a ScrollView. The end result I am trying to achieve is that of Instagram's profile view.
I'd imagine the view's implementation would be something like this:
ScrollView {
VStack {
HStack {
// Profile Pic
// Stats
}
// Bio
// Button(s)
LazyVStack(pinnedViews: .sectionHeaders) {
Section {
TabView {
// Tab 1
// Tab 2
// Tab 3
}
} headers: {
// Tab icons
}
}
}
}
The problem is, the TabView never appears. Also I am unsure whether this is the best setup especially the LazyVStack as I am only using to for the fact that it pins the headers. As I said previously, I'm super new to SwiftUI so there are definetly some views that I have no idea exist some of which might be useful in what I am trying to achieve.
Nonetheless, how can I achieve the layout I am going for?
Thank you!
Side Question: With the way that I have the view setup, the scrollbar for the view is for the entire view however in apps like Instagram, the scrollbars are only within the tabs themselves. How could I also incorporate that aspect into the solution? Thanks again!
You are on the right track. TabView when not used as the root loses the ability to resize it self properly.
So that's why we need to specfiy its minHeight. You can use GeometryReader for that.
Then just give TabView a selection in order for the Tab-Buttons to work and most likely you want to apply .tabViewStyle(.page(indexDisplayMode: .never)) in order to be able to swipe between them.
Working Demo:
GeometryReader { proxy in
ScrollView {
VStack {
HStack {
Image(systemName: "person.circle")
Text("Some Text")
}
}
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
Section {
TabView(selection: $tabIndex) {
VStack {
Spacer()
Text("1")
Spacer()
}
.frame(maxWidth: .infinity)
.background(.red.opacity(0.5))
.tag(0)
VStack {
Spacer()
Text("2")
Spacer()
}
.frame(maxWidth: .infinity)
.background(.green.opacity(0.5))
.tag(1)
VStack {
Spacer()
Text("3")
Spacer()
}
.frame(maxWidth: .infinity)
.background(.blue.opacity(0.5))
.tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(maxWidth: .infinity, minHeight: proxy.size.height)
} header: {
HStack {
Button {
withAnimation {
tabIndex = 0
}
} label: {
Text("Tab 1")
}
.buttonStyle(.bordered)
Button {
withAnimation {
tabIndex = 1
}
} label: {
Text("Tab 2")
}
.buttonStyle(.bordered)
Button {
withAnimation {
tabIndex = 2
}
} label: {
Text("Tab 3")
}
.buttonStyle(.bordered)
}
.padding()
.frame(maxWidth: .infinity)
.background(.regularMaterial)
}
}
}
}
As to your side question: They most likely still use a single scrollview but then move the inset of the scroll indicators. You cannot do that with SwiftUI only but you would need to introspect the underlying UIScrollView for that. SwiftUI-Introspect is good for that.
And then adjust the verticalScrollIndicatorInsets to your needs
HStack {
textfield("", text: $text)
Text("Something")
}
.onTapGesture {
code
}
How can I focus the textfield without executing the codes in onTapGesture?
please help me, thanks a lot.
I come with following idea, it's simple and maybe temporary solution. Just Add onTapGesture Action on TextField so it will execute that and does not execute action on HStack.
HStack {
TextField("", text: $text).onTapGesture {
print("Tap On TextField")
}
Text("Something")
}
.onTapGesture {
print("Tap On HStack")
}
If we tap on Text("Something") it will execute HStack onTapGesture Action.
Update : -
WE can also do something like following :
HStack {
TextField("", text: $text).onTapGesture {
print("Tap On TextField")
}
Group {
Text("Something")
// Add Other Item here
}.onTapGesture {
print("Tap On Group")
}
}
This simply make 2 groups in Hstack and on Textfield Tap if you don't want any action then simply don't put any action on it. Put Your action on Other Group.
The newly introduced swipeActions modifier does not work directly with the List container that presents the rows of data arranged. However, it works perfectly fine in case I use a ForEach — A structure that computes views on demand from an underlying collection of identified data.
Below code doesn't work and show swipeActions
struct SwipeButtonDemoView: View {
let listItems = WWDCViewModel().sessions
var body: some View {
NavigationView {
VStack {
Spacer()
List(listItems) { session in
HStack {
Image(systemName: "play")
Text(session.title)
.font(.callout)
}
.swipeActions(edge: .leading) {
Button {
print("Bookmark")
} label: {
Label("Bookmark", systemImage: "bookmark")
}.tint(.indigo)
}
}
.listRowSeparator(.hidden)
}
.listStyle(.inset)
.navigationTitle("WWDC 21")
}
}
}
Below code works and shows swipeActions..
struct SwipeButtonDemoView: View {
let listItems = WWDCViewModel().sessions
var body: some View {
NavigationView {
VStack {
Spacer()
List {
ForEach(listItems) { session in
HStack {
Image(systemName: "play")
Text(session.title)
.font(.callout)
}
.swipeActions(edge: .leading) {
Button {
print("Bookmark")
} label: {
Label("Bookmark", systemImage: "bookmark")
}.tint(.indigo)
}
}
.listRowSeparator(.hidden)
}
}
.listStyle(.inset)
.navigationTitle("WWDC 21")
}
}
}
Why it's not working with List directly? However, it works as expected with ForEach!!!
You are right that in Xcode 13 beta 1, swipeActions can't be applied to the internal block declaration of a List, so this version of your example wouldn't work:
List(listItems) { session in
HStack {
Image(systemName: "play")
Text(session.title)
.font(.callout)
}
.swipeActions(edge: .leading) {
// etc
}
}
It would appear that there'd be a valid use case for making swipe actions available for List declarations like this, so it's worth using the Feedback Assistant to make that suggestion to Apple.
I'm building a SwiftUI app that has a Picker view. It is embedded inside a NavigationView, so as I tap on the title of the Picker, I get forwarded to a fullscreen selection view to select a value. I can select one only by tapping on the Text itself, but nothing happens if I tap outside the red area.
I wonder is this an expected behaviour or some kind of a bug? Can it be fixed so tap on any part of the row will count as a selection?
The code is following:
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", text: $checkAmount)
.keyboardType(.decimalPad)
// !!!
// The picker is here
// !!!
Picker("Number of people", selection: $peopleIndex) {
ForEach(peopleRange) {
Text("\($0) people")
}
}
}
Section(header: Text("How much tip do you want to leave?")) {
Picker("Tip percentage", selection: $percentageIndex) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}.pickerStyle(SegmentedPickerStyle())
}
Section {
Text("$\(totalPerPerson, specifier: "%.2f")")
}
}.navigationBarTitle("WeSplit")
}
}
Try .contentShape(Rectangle())
ForEach(peopleRange) {
HStack {
Text("\($0) people")
Spacer()
}
.contentShape(Rectangle())
}
It seems that this behaviour is a bug and will be fixed in the Xcode 11.2
If you want to select all the row you can use an HStack with Spacer:
ForEach(peopleRange) {
HStack {
Text("\($0) people")
Spacer()
}
}
Hope this help you or give you some ideas.
Picker("Number of people", selection: $peopleIndex) {
ForEach(1..<100) { index in
ZStack{
Color.white
Text("\(index) people")
}
}
}
I've got a List view and each row of the list contains an HStack with some text view('s) and an image, like so:
HStack{
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}.tapAction { self.groupSelected(self.group) }
This seems to work great, except when I tap in the empty section between my text and the image (where the Spacer() is) the tap action is not registered. The tap action will only occur when I tap on the text or on the image.
Has anyone else faced this issue / knows a workaround?
As I've recently learned there is also:
HStack {
...
}
.contentShape(Rectangle())
.onTapGesture { ... }
Works well for me.
Why not just use a Button?
Button(action: { self.groupSelected(self.group) }) {
HStack {
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}
}.foregroundColor(.primary)
If you don't want the button to apply the accent color to the Text(group.name), you have to set the foregroundColor as I did in my example.
works like magic on every view:
extension View {
func onTapGestureForced(count: Int = 1, perform action: #escaping () -> Void) -> some View {
self
.contentShape(Rectangle())
.onTapGesture(count:count, perform:action)
}
}
The best approach in my opinion for accessibility reasons is to wrap the HStack inside of a Button label, and in order to solve the issue with Spacer can't be tap, you can add a .contentShape(Rectangle()) to the HStack.
So based on your code will be:
Button {
self.groupSelected(self.group)
} label: {
HStack {
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}
.contentShape(Rectangle())
}
I've been able to work around this by wrapping the Spacer in a ZStack and adding a solid color with a very low opacity:
ZStack {
Color.black.opacity(0.001)
Spacer()
}
Simple extension based on Jim's answer
extension Spacer {
/// https://stackoverflow.com/a/57416760/3393964
public func onTapGesture(count: Int = 1, perform action: #escaping () -> Void) -> some View {
ZStack {
Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
self
}
}
}
Now this works
Spacer().onTapGesture {
// do something
}
I just add the background color(except clear color) for HStack works.
HStack {
Text("1")
Spacer()
Text("1")
}.background(Color.white)
.onTapGesture(count: 1, perform: {
})
Although the accepted answer allows the mimicking the button functionality, visually it does not satisfy. Do not substitute a Button with a .onTapGesture or UITapGestureRecognizer unless all you need is an area which accepts finger tap events. Such solutions are considered hacky and are not good programming practices.
To solve your problem you need to implement the BorderlessButtonStyle ⚠️
Example
Create a generic cell, e.g. SettingsNavigationCell.
SettingsNavigationCell
struct SettingsNavigationCell: View {
var title: String
var imageName: String
let callback: (() -> Void)?
var body: some View {
Button(action: {
callback?()
}, label: {
HStack {
Image(systemName: imageName)
.font(.headline)
.frame(width: 20)
Text(title)
.font(.body)
.padding(.leading, 10)
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.font(.headline)
.foregroundColor(.gray)
}
})
.buttonStyle(BorderlessButtonStyle()) // <<< This is what you need ⚠️
}
}
SettingsView
struct SettingsView: View {
var body: some View {
NavigationView {
List {
Section(header: "Appearance".text) {
SettingsNavigationCell(title: "Themes", imageName: "sparkles") {
openThemesSettings()
}
SettingsNavigationCell(title: "Lorem Ipsum", imageName: "star.fill") {
// Your function
}
}
}
}
}
}
I've filed feedback on this, and suggest you do so as well.
In the meantime an opaque Color should work just as well as Spacer. You will have to match the background color unfortunately, and this assumes you have nothing to display behind the button.
Kinda in the spirit of everything that has been said:
struct NoButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(Color.black.opacity(0.0001))
}
}
extension View {
func wrapInButton(action: #escaping () -> Void) -> some View {
Button(action: action, label: {
self
})
.buttonStyle(NoButtonStyle())
}
}
I created the NoButtonStyle because the BorderlessButtonStyle was still giving an animation that was different than .onTapGesture
Example:
HStack {
Text(title)
Spacer()
Text("Select Value")
Image(systemName: "arrowtriangle.down.square.fill")
}
.wrapInButton {
isShowingSelectionSheet = true
}
Another option:
extension Spacer {
func tappable() -> some View {
Color.blue.opacity(0.0001)
}
}
Updated:
I've noticed that Color doesn't always act the same as a Spacer when put in a stack, so I would suggest not using that Spacer extension unless you're aware of those differences. (A spacer pushes in the single direction of the stack (if in a VStack, it pushes vertically, if in a HStack, it pushes out horizontally, whereas a Color view pushes out in all directions.)
This website helped answer the same question for me:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape
Try applying .ContentShape(Rectangle()) to the HStack.