Unable to remove space between two Horizontal Scroll views in SwiftUI? - ios

I am trying to create two horizontal scrollable list of elements. Currently, this is what I am able to achieve:
As can be seen, I want to remove the space between the two Scroll Views. Here, is the code I am using to achieve this:
var body: some View {
NavigationView {
VStack (spacing: 0){
Text("Zara") .font(.custom(
"AmericanTypewriter",
fixedSize: 36))
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}.task {
await loadData()
}
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}
}
}
}
HorizontalScrollModel() returns a LazyHGrid of the products. I have tried setting the spacing to 0 with VStack (spacing: 0) but this did not work. How do I remove the space between the ScrollViews?
HorizontalScrollModel:
struct HorizontalScrollModel: View {
var searchResults1: [homeResult]
let columns = [
GridItem(.adaptive(minimum: 200))]
#State private var isHearted = false
init( searchResult: [homeResult]) {
self.searchResults1 = searchResult
}
var body: some View {
LazyHGrid (rows: columns ){
ForEach(searchResults1, id: \.prodlink) { item in
NavigationLink{
ProductView(imageLink: item.image_src, productLink: item.prodlink ,
productCost: String(item.price))
} label: {
VStack (alignment: .leading, spacing: 0){
AsyncImage(url: URL(string: item.image_src)) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 10))
} else if phase.error != nil {
Text("There was an error loading the image.")
} else {
ProgressView()
}
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.gray.opacity(0.2), lineWidth: 4)
)
.padding(.top, 2)
.padding(.bottom, 2)
.padding(.leading, 2)
.padding(.trailing, 2)
.overlay (
RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.3), lineWidth: 1))
}
}
}
// .padding([.horizontal, .bottom])
}
}
}

Each ScrollView takes all available space offered by VStack, which since it has 2 views will be half the screen for each view.
Inside each ScrollView you then place a LazyHGrid, and inside the cell you are placing a 150x150 image, but the ScrollView size will not change to fit the image.
So, it now depends on what you're aiming for. If you just want the two ScrollView to be joined together, you can have a .frame modified to the outer VStack with a height of 308 (150 for each image + the padding).

Try adding the .fixedSize() modifier to the ScrollView:
ScrollView(.horizontal) {
HorizontalScrollModel(searchResult: searchResults1)
}
.fixedSize(horizontal: false, vertical: true)
You might also be able to add it just to the HorizontalScrollModel.

Related

Prevent SwiftUI scrollview scrolling when interacting with a specific component

I am trying to add a PKCanvasView to a scrollview with SwiftUI.
But when I try to draw in the PKCanvasView it scrolls the scrollview instead.
In other words, how could I deactivate the scrolling when the user interacts with a specific view inside the scrollview?
I parsed through a lot of examples and blog articles and almost similar things but they were either too old or not for SwiftUI...
EDIT
To give further details (and based on Guillermo answer), here is an additional sample:
struct ContentView: View {
#State private var scrollDisabled = false
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill((i & 1 == 0) ? .blue: .yellow)
.frame(width: 300, height: 50)
.overlay {
Text("\(i)")
}
}
}.frame(maxWidth: .infinity)
}
.scrollDisabled(scrollDisabled)
}
.frame(width: 300)
}
}
Note that you cannot scroll touching the leading/trailing white borders.
I'd like the same behavior for yellow items: if you try to move up/down while touching a blue cell it scrolls, but if you try the same with yellow the scrollview shouldn't scroll!
Sorry, I'm really trying my best to be clear... ^^
You can control how your elements respond to the drag gesture with the following:
.gesture(DragGesture(minimumDistance:))
For instance:
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill((i & 1 == 0) ? .blue: .yellow)
.frame(width: 300, height: 50)
.overlay {
Text("\(i)")
}
.gesture(DragGesture(minimumDistance: i & 1 == 0 ? 100 : 0))
}
}.frame(maxWidth: .infinity)
}
}
.frame(width: 300)
}
}
will block the scrolling for the yellow elements!
In iOS 16 was added a modifier scrollDisabled to achieve what you need here's an example:
struct Scroll: View {
#State private var scrollDisabled = false
var body: some View {
VStack {
Button("\(scrollDisabled ? "Enable" : "Disable") Scroll") {
scrollDisabled.toggle()
}
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill(.blue)
.frame(width: 50, height: 50)
.overlay {
Text("\(i)")
}
}
}.frame(maxWidth: .infinity)
}
.scrollDisabled(scrollDisabled)
}
}
}

Lazy loading SwiftUI Grid on both vertical and horizontal direction

As the title says I try to build a Grid view that loads lazily on both vertical and horizontal, the main goal is building a calendar for a list of employee, both the list and the calendar interval can be very large so they have to be rendered lazily. I've tried LazyHGrid and LazyVGrid but they only render views lazily on one direction. The further approach uses one LazyVStack for the whole view and one LazyHStack for each row, the loading is lazily (check print in the console) but while scrolling the views loses their position and the grid get's broken
struct ContentView: View {
var body: some View {
ScrollView([.horizontal, .vertical]) {
LazyVStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: columnHeaders) {
ForEach(0..<20) { row in
LazyHStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: rowHeader(with: "Employee \(row)")) {
ForEach(0..<100) { column in
Text("Cell \(row), \(column)")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 200, height: 100)
.background(Color.red)
.onAppear {
print("Cell \(row), \(column)")
}
}
}
}
}
}
}
}
}
var columnHeaders: some View {
LazyHStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: rowHeader(with: "")) {
ForEach(0..<100) { column in
Text("Header \(column)")
.frame(width: 200, height: 100)
.background(Color.white)
}
}
}
}
func rowHeader(with label: String) -> some View {
Text(label)
.frame(width: 100, height: 100)
.background(Color.white)
}
}
PS: I also need fixed headers, that is achieved in the code above using pinnedViews
You can fix the out-of-alignment problem by using a pair of nested single-axis ScrollViews instead of one multi-axis ScrollView.

How to show Profile Icon next to Large Navigation Bar Title in SwiftUI?

I am developing an App that supports multiple Profiles. I really like the way Apple displays the Profile Icon next to the Large Navigation Bar Title in all their Apps. See the Screenshot below:
My Question is the following:
Is it possible to achieve this in SwiftUI? And if so, how?
If it's not possible in pure SwiftUI, how can I achieve it including UIKit Code?
Thanks for your help.
I solved this by using SwiftUI-Introspect, to "Introspect underlying UIKit components from SwiftUI".
Here is an example of a view:
struct ContentView: View {
#State private var lastHostingView: UIView!
var body: some View {
NavigationView {
ScrollView {
ForEach(1 ... 50, id: \.self) { index in
Text("Index: \(index)")
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Large title")
.introspectNavigationController { navController in
let bar = navController.navigationBar
let hosting = UIHostingController(rootView: BarContent())
guard let hostingView = hosting.view else { return }
// bar.addSubview(hostingView) // <--- OPTION 1
// bar.subviews.first(where: \.clipsToBounds)?.addSubview(hostingView) // <--- OPTION 2
hostingView.backgroundColor = .clear
lastHostingView?.removeFromSuperview()
lastHostingView = hostingView
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8)
])
}
}
}
}
Bar content & profile picture views:
struct BarContent: View {
var body: some View {
Button {
print("Profile tapped")
} label: {
ProfilePicture()
}
}
}
struct ProfilePicture: View {
var body: some View {
Circle()
.fill(
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
.padding(.horizontal)
}
}
The .frame(width: 40, height: 40) & hostingView.bottomAnchor constant will need to be adjusted to your needs.
And the results for each option (commented in the code):
Option 1
Option 2
View sticks when scrolled
View disappearing underneath on scroll
Without NavigationView
I done this with pure SwiftUI. You have to replace the Image("Profile") line with your own image (maybe from Assets or from base64 data with UIImage).
HStack {
Text("Apps")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
.padding(.all, 30)
This products following result:
With NavigationView
Let's assume that you have NavigationView and inside that there's only ScrollView and .navigationTitle. You can add that profile image there by using overlay.
NavigationView {
ScrollView {
//your content here
}
.overlay(
ProfileView()
.padding(.trailing, 20)
.offset(x: 0, y: -50)
, alignment: .topTrailing)
.navigationTitle(Text("Apps"))
}
Where ProfileView could be something like this:
struct ProfileView: View {
var body: some View {
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
}
The result will be like this...
...which is pretty close to the App Store:

SwiftUI: making forever animation of y offset in a filled horizontal image

I'm trying to animate an image inside a "row" by it's y axis so that it appears that you would the view would slowly scroll vertically through the entire image. And when done, it backtracks. I hope that makes sense. I'm trying to do it in this code:
var body: some View {
ZStack {
HStack {
GeometryReader { geometry in
WebImage(url: self.url)
.renderingMode(.original)
.resizable()
.placeholder {
ImageStore.shared.image(name: "exploras-icon")
}
.aspectRatio(contentMode: .fill)
.animate {
// animate by y offset within bounds of HStack
}
}
}
.frame(height: 140)
.clipped()
}
}
Any help/guidance much appreciated!
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4
struct TestAutoScrollImage: View {
#State private var isActive = false
var body: some View {
GeometryReader { gp in
HStack {
Image("large_image")
}
.frame(width: gp.size.width, height: gp.size.height, alignment: self.isActive ? .bottom : .top)
}
.edgesIgnoringSafeArea([.top, .bottom])
.animation(Animation.linear(duration: 5).repeatForever())
.onAppear {
self.isActive = true
}
}
}

How to make a Horizontal List in SwiftUI?

I can wrap all my views inside a List
List {
// contents
}
But this seems to be vertical scrolling. How do I make it horizontal?
You need to add .horizontal property to the scrollview. otherwise it won't scroll.
ScrollView (.horizontal, showsIndicators: false) {
HStack {
//contents
}
}.frame(height: 100)
Starting from iOS 14 beta1 & XCode 12 beta1 you will be able to wrap LazyHStack in a ScrollView to create a horizontal lazy list of items, i.e., each item will only be loaded on demand:
ScrollView(.horizontal) {
LazyHStack {
ForEach(0...50, id: \.self) { index in
Text(String(index))
.onAppear {
print(index)
}
}
}
}
To make a horizontal scrollable content, you can wrap a HStack inside a ScrollView:
ScrollView {
HStack {
ForEach(0..<10) { i in
Text("Item \(i)")
Divider()
}
}
}
.frame(height: 40)
You can use .horizontal property and use custom elements. For me I use cusomt CircleView
var body: some View {
VStack{
Divider()
ScrollView(.horizontal){
HStack(spacing:10){
ForEach(0..<10){
index in
CircleView(label: "\(index)")
}
}.padding()
}.frame(height:100)
Divider()
Spacer()
}
}
}
//struct CircleView:View
#State var label:String
var body:some View {
ZStack{
Circle()
.fill(Color.yellow)
.frame(width: 70, height: 70)
Text(label)
}
}
}

Resources