How to put tabs inside a scrollview in SwiftUI? - ios

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

Related

SwiftUI onTapGuesture not working using offset inside ZStack?

I have a Mapbox map view and a search bar and activate button (HStack) inside a ZStack and using an offset modifier to position them at the top of the screen.
For some reason the offset is preventing the onTapGesture from working for the activate button, if I comment out the offset it will work but will be placed at the bottom of the screen.
I tried adding the offset to each element individually inside the HStack but that did not work...
How can I make the onTapGesture functionality work with offset?
Thank you
struct MapScreen: View {
var body: some View {
NavigationView {
ZStack(alignment: .bottom) {
HStack(spacing: 10) {
SearchBar()
.padding(.leading, 5)
.onTapGesture {
print("search pressed")
}
ActivateButton()
.onTapGesture {
print("activate pressed")
}
}.zIndex(2)
.offset(y: -770)
MapView(locations: $locations)
.edgesIgnoringSafeArea([.top, .bottom])
BottomNavBar().zIndex(1)
.edgesIgnoringSafeArea(.all).offset(y: 35)
}
.navigationViewStyle(StackNavigationViewStyle())
.fullScreenCover(isPresented: $presentSearchView, content: {
SearchView()
})
}
}
}
There's a couple problems here:
.offset(y: -770)
If you're trying to use an offset so large, you shouldn't be using offset at all. offset is usually for fine-tune adjustments and doesn't work great with big values. And also, 770 is hardcoded. What happens when you use another device with a different screen size? Don't hardcode or do calculations yourself — SwiftUI can do it for you!
Instead, use a VStack + Spacer() to push the search bar up.
ZStack(alignment: .bottom) {
VStack { /// here!
HStack(spacing: 10) {
SearchBar()
.padding(.leading, 5)
.onTapGesture {
print("search pressed")
}
ActivateButton()
.onTapGesture {
print("activate pressed")
}
}
Spacer() /// push the `HStack` to the top of the screen
}
.zIndex(2)
MapView(locations: $locations)
.edgesIgnoringSafeArea([.top, .bottom])
BottomNavBar().zIndex(1)
.edgesIgnoringSafeArea(.all).offset(y: 35)
}

make Button action cover it's superview swiftUI

i need to make a button that fills it's container view and make all the view clickable, but i failed, it only make button content size clickable (which is the label size)
Button(action: {
print("item Clicked")
}) {
Text("whyyyy")
}.frame(maxWidth: .infinity, maxHeight: .infinity).buttonStyle(PlainButtonStyle())
i know i can reverse it by putting my container view within the button but i just don't like this style i need the button to be within the view
and i know i can make a tapGesture but i want to feel free like in UIKit
You can use clear color content to make whole area clickable.
struct ContentViewNewTest: View {
var body: some View {
ZStack {
Color.red
Button(action: {
print("item Clicked")
}) {
Color.clear //<--- Here
}
}
}
}
Just move .frame(maxWidth: .infinity, maxHeight: .infinity) under Text("whyyyy") before before closing the bracket
Edit:
Also add the modifier .background(Color.clear)
Final Result:
Button(action: {
print("item Clicked")
}) {
Text("whyyyy")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.clear)
}

Present Modal fullscreem SwiftUI

how can i present a modal that will take up the fullscreen and can't be dismissed by swiping it down? Currently I am using .sheet on a view to present a modal that is dismissible.
I haven't noticed any beta changes in Xcode that changes this behavior.
Any help would be appreciated :)
SwiftUI 1.0
I'm not sure if this what you'd want to go with but it's possible to create your own modal screen by using the ZStack and a state variable to control the hiding/showing of it.
Code
struct CustomModalPopups: View {
#State private var showingModal = false
var body: some View {
ZStack {
VStack(spacing: 20) {
Text("Custom Popup").font(.largeTitle)
Text("Introduction").font(.title).foregroundColor(.gray)
Text("You can create your own modal popup with the use of a ZStack and a State variable.")
.frame(maxWidth: .infinity)
.padding().font(.title).layoutPriority(1)
.background(Color.orange).foregroundColor(Color.white)
Button(action: {
self.showingModal = true
}) {
Text("Show popup")
}
Spacer()
}
// The Custom Popup is on top of the screen
if $showingModal.wrappedValue {
// But it will not show unless this variable is true
ZStack {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.vertical)
// This VStack is the popup
VStack(spacing: 20) {
Text("Popup")
.bold().padding()
.frame(maxWidth: .infinity)
.background(Color.orange)
.foregroundColor(Color.white)
Spacer()
Button(action: {
self.showingModal = false
}) {
Text("Close")
}.padding()
}
.frame(width: 300, height: 200)
.background(Color.white)
.cornerRadius(20).shadow(radius: 20)
}
}
}
}
}
Example
(Excerpt from "SwiftUI Views" book)
So here, your popup is small, but you can adjust the dimensions to make it fullscreen with the frame modifier that is on that VStack.

Navigation stuff in SwiftUI

I'm trying to figure out how to use the navigation bar in SwiftUI
I want to put BarButtonItem and images inside the NavigationBar
I have been able to display the navigation bar and put titles
var body: some View {
NavigationView{
List(0...5) { note in
VStack(alignment: .leading) {
Text("title")
Text("Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.navigationBarTitle(Text("Notes"))
}
}
iOS 14
You should use the toolbar modifier:
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { /* action */ }
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { /* Actions */ }, label: {
HStack {
Image(systemName: "trash")
Text("Delete")
}
})
.foregroundColor(.red) // You can apply colors and other modifiers too
}
}
Note 1: You can have ANY View there. (not only a Button) and also any modifiers
Note 2: Both codes above and below will generate the same look items but with different approachs
iOS 13 and above (deprecated but still works)
You should use .navigationBarItems() modifier. For example you can add Button or Image like this:
.navigationBarItems(
leading: Button("Cancel") {
// Actions
},
trailing: Button(action: {
// Actions
}, label: { Label("Delete", systemImage: "trash") }
).foregroundColor(.red) // You can apply colors and other modifiers too
)
💡 Pro TIP
Always try to encapsulate each item in a separated struct, so your code will be simplified and very easy to replace with newer technologies. for example, take a look at this sample:
.navigationBarItems(
leading: MyCustomButtonItem(),
trailing: MyCustomButtonItem(text: "foo", image: "Bard")
)
.navigationBarItems() is the function you are looking for. You can specify a leading view, trailing view, or both. Within the view, you can specify horizontal and vertical stacks to add additional buttons.
var body: some View {
NavigationView{
List(0...5) { note in
VStack(alignment: .leading) {
Text("title")
Text("Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.navigationBarItems(leading: HStack {
Button(action: {}, label: {Image(systemName: "star.fill")})
Button(action: {}, label: {Text("Edit")})
}, trailing: VStack {
Button(action: {}, label: {Image(systemName: "star.fill")})
Button(action: {}, label: {Text("Edit")})
})
.navigationBarTitle(Text("Notes"))
}
}
SwiftUI 2
In SwiftUI 2 / iOS 14 the navigationBarItems modifier is deprecated.
Instead we should use a toolbar with ToolbarItems.
NavigationView {
List {
// ...
}
.navigationTitle("Notes")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Tap me") {
// action
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Image(systemName: "plus")
}
}
}
You can see the documentation for more ToolbarItemPlacements.
Check the image here
The toolbar() modifier lets us add single or multiple bar button items to the leading and trailing edge of a navigation view, as well as other parts of our view if needed. These might be tappable buttons, but there are no restrictions – you can add any sort of view.
var body: some View {
NavigationView {
ScrollView {
VStack{
}//: VStack
}//: Scroll
.navigationTitle("Settings")
.toolbar(content: {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
}
}
})
.padding()
}//: Navigation
}
put this from parentViewController
NavigationLink(destination: NotesListView())

How to put a logo in NavigationView in SwiftUI?

I am trying to use a logo image instead of a NavigationView title at the top section of the app. Couldn't find any documentation of using images inside a NavigationView.
iOS 14+
Starting from iOS 14 you can create a ToolbarItem with the principal placement:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Test")
.toolbar {
ToolbarItem(placement: .principal) {
Image(systemName: "ellipsis.circle")
}
}
}
}
}
See the ToolbarItemPlacement documentation for more placements.
NavigationView.navigationBarTitle() can only take a Text() argument right now. You could instead use .navigationBarItems() to set an Image as either the trailing or leading argument, but this is the SwiftUI equivalent of UINavigationItem.leftBarButtonItem[s] and UINavigationItem.rightBarButtonItem[s], which means that you're restricted to navigation bar button dimensions. But if you're ok with that, you may want to set a blank title so that you can specify a standard-height navigation bar.
Hard-Coded Positioning
If you can stand to live with yourself, you can fake a centered nav bar item by hard-coding padding around the image, like
.padding(.trailing, 125),
(Note that I deliberately positioned it off-center so that you can see that it's hard-coded.)
Slightly Less Hard-Coded Positioning
Even better would be to wrap the whole thing in a GeometryReader { geometry in ... } block to use the screen dimensions to calculate precise positioning, if you know the exact width of the image you're using:
GeometryReader { geometry in
NavigationView {
...
}
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(.trailing, (geometry.size.width / 2.0) + -30), // image width = 60
destination: ProfileHost()
)
)
If you don't want to hack it, here's what you can do:
Standard nav bar height, left button item
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(),
destination: ProfileHost()
)
)
Standard nav bar height, right button item
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(),
destination: ProfileHost()
)
)
Expanded nav bar height, no title, left button item
.navigationBarItems(leading:
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.padding(),
destination: ProfileHost()
)
)
Use this:
NavigationView {
Text("Hello, SwiftUI!")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
Image(systemName: "sun.min.fill")
Text("Title").font(.headline)
}
}
}
}
Credit: https://sarunw.com/posts/custom-navigation-bar-title-view-in-swiftui/
With SwiftUIX, you can use navigationBarTitleView(View):
NavigationView() {
NavigationLink(destination:YourView().navigationBarTitleView(Image(systemName: "message.fill")))
}
I don't want to claim 100% accuracy whether title image positioned at center but visually it looks center to me. Do your judgment and adjust padding :)
Here is code:
.navigationBarTitle(
Text("")
, displayMode: .inline)
.navigationBarItems(leading:
HStack {
Button(action: {
}) {
Image(systemName: "arrow.left")
}.foregroundColor(Color.oceanWhite)
Image("oceanview-logo")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 40, alignment: .center)
.padding(UIScreen.main.bounds.size.width/4+30)
}
,trailing:
HStack {
Button(action: {
}) {
Image(systemName: "magnifyingglass")
}.foregroundColor(Color.oceanWhite)
}
)
To extend on NRitH's answer, putting your logo in a different component (to borrow a React way of putting it) may help anyone looking to understand the concepts.
The actual Image can be wrapped in any container view such as a VStack, etc. An example of setting up a struct as a component to be used in our navigation items could be something like the following:
struct NavLogo: View {
var body: some View {
VStack {
Image("app-logo")
.resizable()
.aspectRatio(2, contentMode: .fit)
.imageScale(.large)
}
.frame(width: 200)
.background(Color.clear)
}
}
When the aspect ratio is set, only the width needs to be set on the frame on the container view. We could also set a property in the NavLogo to set width and/or height from property dependency injection. Regardless, our navigationBarItems becomes very straight forward and more readable 🙂
NavigationView {
Text("Home View")
.navigationBarItems(
leading: NavLogo()
trailing: ProfileButton()
)
}
On iOS 13, a little hacky way to achieve this:
private var logo: some View {
Image("logo-image")
}
var body: some View {
GeometryReader { g in
content()
.navigationBarTitle("")
.navigationBarItems(leading:
ZStack(alignment: .leading) {
logo.frame(width: g.size.width).padding(.trailing, 8)
HStack {
leadingItems().padding(.leading, 10)
Spacer()
trailingItems().padding(.trailing, 10)
}
.frame(width: g.size.width)
}
)
}
}
Try the following.
struct ContainerView: View {
var body: some View {
VStack {
Image(systemName: "person.crop.square")
ContentView()
}
}
}
It worked for me.
Make sure you change ContentView to ContainerView inside SceneDelegate.swift before running on simulator or device.

Resources