I am creating scrollview as follows and I want to go requested button in array
using Namespace variable.
I saw it used to to go to bottom or top of scrollview but Namespace variables created manually. Because I want to go any button might requested I want to create Namespace
for every item in scrollview not only for top or bottom one.
Is it possible to create Namespace ID using index somehow valueIndex in that case.
I hope it would be something like that selectIndex as Namespace.
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: true){
LazyHStack() {
ForEach(0..<listValues[valueIndex].count, id: \.self) { selectIndex in
Button(action: {
dump(selectIndex)
}){
Text(listValues[valueIndex][selectIndex])
.foregroundColor(Color.white)
.background(Color.red)
.frame(width: w2 * 0.5 * 0.5, height: 40)
} .id(**selectIndex as Namespace**)
}.background(Color.gray)
}
}.frame(width: w2 * 0.5 * 0.5, height: 40)
}
You don't need a NameSpace in a ScrollViewReader, you just need to use any Hashable to identify the place you want to scroll to. You didn't give a Minimal Reproducible Example (MRE), and I couldn't find a straightforward scroll to answer, so I made a basic one to show you how it is done. Please see the comments.
// You should always use an Identifiable in a ForEach. For
// a `scrollTo()`, you only need a `Hashable` `.id()`, so
// this struct has been conformed to Hashable.
struct ScrollItem: Identifiable, Hashable {
let id = UUID()
var name: String
}
struct ScrollToView: View {
let scrollItems: [ScrollItem] = Array(0..<100).map( { ScrollItem(name: "Row " + $0.description) })
#State var selectedItem: ScrollItem?
var body: some View{
ScrollViewReader{ scrollReader in
VStack {
// This button scrolls the selected row to the top, or as high as it can go
Button {
withAnimation {
scrollReader.scrollTo(selectedItem, anchor: .top)
}
} label: {
Text(selectedItem != nil ? "Scroll \(selectedItem!.name) To Top" : "Select Row")
}
List {
// ScrollItem is Identifiable, so you do not need to designate id:
ForEach(scrollItems) { item in
Text(item.name)
.padding()
.frame(maxWidth: .infinity, minHeight: 50, alignment: .leading)
// If the row is selected, this gives a colored background
.background(
RoundedRectangle(cornerRadius: 15)
.fill(selectedItem == item ? Color.yellow.opacity(0.4) : Color.clear)
)
.contentShape(RoundedRectangle(cornerRadius: 15))
// This allows you to select and deselect the row
.onTapGesture {
if selectedItem == item {
selectedItem = nil
} else {
selectedItem = item
}
}
// ScrollItem conforms to Hashable, so it can be used as the .id()
.id(item)
}
}
// This button scrolls the selected row to the bottom, or as low as it can go
Button {
withAnimation {
scrollReader.scrollTo(selectedItem, anchor: .bottom)
}
} label: {
Text(selectedItem != nil ? "Scroll \(selectedItem!.name) To Bottom" : "Select Row")
}
}
}
}
}
Related
I want to recreate the animated buttons found in the Photos app and shown below. My goal is to use this type of buttons in a TabView(or something similar) instead of the default ones. Does these type of buttons exist in swiftUI? Or what is a good way to create these buttons?
I have written some stupid code to illustrate the problem, but it feels like the wrong approach.
struct ContentView: View {
#State private var selection = 0
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.frame(width: 100, height: 50)
.offset(x: CGFloat(selection * 100), y: 0)
HStack {
Button("Tap Me") {
withAnimation {
selection = 0
}
}
Spacer()
Button("Tap Me") {
withAnimation {
selection = 1
}
}
Spacer()
Button("Tap Me") {
withAnimation {
selection = 2
}
}
}
.frame(width: 300)
}
.padding()
.background(.green)
}
}
Output:
I have added the .id(1) to the positions in the scrollview and can get it to work as expected if i add a button inside the scrollview but i want to use a picker to jump to the .id and outside the scrollview.
Im new to this.
I have this code:
if i use this button it works as expected although its placed inside the scrollview...
Button("Jump to position") {
value.scrollTo(1)
}
This is my picker...
// Main Picker
Picker("MainTab", selection: $mainTab) {
Text("iP1").tag(1)
Text("iP2").tag(2)
Text("Logo").tag(3)
Text("Canvas").tag(4)
}
.frame(width: 400)
.pickerStyle(.segmented)
ScrollViewReader { value in
ScrollView (.vertical, showsIndicators: false) {
ZStack {
RoundedRectangle(cornerRadius: 20)
// .backgroundStyle(.ultraThinMaterial)
.foregroundColor(.black)
.opacity(0.2)
.frame(width: 350, height:185)
// .foregroundColor(.secondary)
.id(1)
There are 2 things you are mixing here:
The tag modifier is to differentiate elements among certain selectable views. i.e. Picker TabView
You can't access the proxy reader from outside unless you make it available. In other words the tag in the Picker and the ScrollViewReader does not have a direct relationship, you have to create that yourself:
import SwiftUI
struct ScrollTest: View {
#State private var mainTab = 1
#State private var scrollReader: ScrollViewProxy?
var body: some View {
// Main Picker
Picker("MainTab", selection: $mainTab) {
Text("iP1").tag(1)
Text("iP2").tag(2)
Text("Logo").tag(3)
Text("Canvas").tag(4)
}
.frame(width: 400)
.pickerStyle(.segmented)
.onChange(of: mainTab) { mainTab in
withAnimation(.linear) {
scrollReader?.scrollTo(mainTab, anchor: .top)
}
}
ScrollViewReader { value in
ScrollView (.vertical, showsIndicators: false) {
ForEach(1...4, id: \.self) { index in
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.black)
.opacity(0.2)
.frame(width: 350, height: 500)
Text("index: \(index)")
}
.id(index)
}
}
.onAppear {
scrollReader = value
}
}
}
}
I have designs for a custom Tab component (Segmented Control). The implementation is pretty basic, but one of the design requirements is for the bar at the bottom to animate between the different options (move on x axis + grow to new text size).
I have the below (WIP) implementation that statically swaps the items, but I am not sure how to get the animation between the items.
Using overlay allows for the bar to dynamically take up the full width of the parent, but I wonder if there needs to be a seperate bar that animates between the items.
Here is the WIP code:
struct Tabs: View {
#Binding var selectedTab: Int
var tabs: [Tab]
init(_ selectedTab: Binding<Int>, tabs: [Tab]) {
self._selectedTab = selectedTab
self.tabs = tabs
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(self.tabs.indices) { tabIndex in
let tab = self.tabs[tabIndex]
Button(action: {
withAnimation {
self.selectedTab = tabIndex
}
}) {
Text(tab.title)
.font(.body.weight(.medium))
.foregroundColor(.blue)
.padding(.bottom, 8)
.padding(.top, 2)
.padding(.horizontal, 4)
.if(tabIndex == self.selectedTab) {
$0.overlay(
Rectangle()
.fill(Color.blue)
.frame(width: .infinity, height: 3),
alignment: .bottom
)
}
}
}
}
}
}
}
and here is the expected design (note the underline bar, that is what I need to animate).
You create a new view under each tab during selection, this will not work. For SwiftUI, these will be different views, so they won't animate the position change.
Instead, I suggest you read this great article about alignment guides, especially the Cross Stack Alignment part.
So, using alignment guides, we can bind one of the view guides, such as center, to the selected center of the tab.
But we also need to get the width somehow. I do this with GeometryReader.
struct Tabs: View {
#State var selectedTab = 0
var tabs: [Tab]
#State private var tabWidths = [Int: CGFloat]()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
VStack(alignment: .crossAlignment, spacing: 0) {
HStack(spacing: 12) {
ForEach(self.tabs.indices) { tabIndex in
let tab = self.tabs[tabIndex]
Button(action: {
withAnimation {
self.selectedTab = tabIndex
}
}) {
Text(tab.title)
.font(.body.weight(.medium))
.foregroundColor(.blue)
.padding(.bottom, 8)
.padding(.top, 2)
.padding(.horizontal, 4)
.if(tabIndex == self.selectedTab) {
$0.alignmentGuide(.crossAlignment) { d in
d[HorizontalAlignment.center]
}
}
}.sizeReader { size in
tabWidths[tabIndex] = size.width
}
}
}
Rectangle()
.fill(Color.blue)
.frame(width: tabWidths[selectedTab], height: 3)
.alignmentGuide(.crossAlignment) { d in
d[HorizontalAlignment.center]
}
}
}
}
}
extension View {
func sizeReader(_ block: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometry -> Color in
DispatchQueue.main.async { // to avoid warning
block(geometry.size)
}
return Color.clear
}
)
}
}
extension HorizontalAlignment {
private enum CrossAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let crossAlignment = HorizontalAlignment(CrossAlignment.self)
}
p.s. Don't use .frame(width: .infinity) to extend the view, use .frame(maxWidth: .infinity) instead. Yes, you must split it into two modifiers if you want to provide a static height.
p.s.s. You should use if modifier very carefully. It's fine in this case, but in most cases it will break your animation, see this article to understand why.
I have this codes for swipe the rows inside a scrollview, at some first try it works fine, but after some scrolling and swipe it starts giving some value for offset even almost zero, and it makes red background shown in edges! I know, probably would some of you came with padding solution up and it would work and cover unwanted red background but even with this unconvinced solution, the text would seen offseted from other ones! I am thinking this is a bug of SwiftUI, otherwise I am very pleased to know the right answer.thanks
import SwiftUI
struct item: Identifiable
{
var id = UUID()
var name : String
var offset : CGFloat = 0
}
struct ContentView: View {
#State var Items: [item] = []
func findIndex(item: item) -> Int
{
for i in 0...Items.count - 1
{
if item.id == Items[i].id
{
return i
}
}
return 0
}
var body: some View {
ScrollView(.vertical, showsIndicators: false)
{
LazyVStack(alignment: .leading)
{
ForEach(Items) { item in
ZStack
{
Rectangle()
.fill(Color.red)
HStack
{
VStack(alignment: .leading)
{
Text(item.name)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.background(Color.yellow)
.offset(x: item.offset)
.gesture(
DragGesture()
.onChanged({ (value) in
Items[findIndex(item: item)].offset = value.translation.width
})
.onEnded({ (value) in
Items[findIndex(item: item)].offset = 0
}))
.animation(.easeInOut)
}
}
}
}
}
}
.onAppear()
{
for i in 0...10
{
Items.append(item(name: "item " + String(i)))
}
}
}
}
It is because drag gesture is handled even if you try to scroll vertically and horizontal transition is applied to your offset. So the solution is to add some kind of boundary conditions that detects horizontal-only drag.
Here is just a simple demo (you can think about more accurate one). Tested with Xcode 12 / iOS 14 as kind of enough for demo.
DragGesture()
.onChanged({ (value) in
// make offset only when some limit exceeds or when it definitely sure
// that you drag in horizontal only direction, below is just demo
if (abs(value.translation.height) < abs(value.translation.width)) {
Items[i].offset = value.translation.width
}
})
Update: forgot that I changed also to use indices to avoid afterwards finding by id, so below is more complete snapshot
ForEach(Items.indices, id: \.self) { i in
ZStack
{
Rectangle()
.fill(Color.red)
HStack
{
VStack(alignment: .leading)
{
Text(Items[i].name)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.background(Color.yellow)
.offset(x: Items[i].offset)
.gesture(
DragGesture()
.onChanged({ (value) in
if (abs(value.translation.height) < abs(value.translation.width)) {
Items[i].offset = value.translation.width
}
})
.onEnded({ (value) in
Items[i].offset = 0.0
}))
.animation(.easeInOut)
}
}
}
If I create this simple ScrollView containing 100 rows and create a button to scroll to row 60, I would like to highlight that I am at row 60, maybe with a background colour or similar. I cannot figure out how to do this.
var body: some View {
ScrollView {
ScrollViewReader { value in
Button("Jump to #60") {
value.scrollTo(60, anchor: .center)
}
ForEach(0..<100) { i in
Text("Line \(i)")
}
}
}
}
Here is possible approach - you introduce state for highlighted row, which can modify by needs, including for scroll to
Tested with Xcode 12b5 / iOS 14
#State private var highlighted: Int?
var body: some View {
ScrollView {
ScrollViewReader { value in
Button("Jump to #60") {
value.scrollTo(60, anchor: .center)
highlighted = 60
}
ForEach(0..<100) { i in
Text("Line \(i)")
.frame(maxWidth: .infinity)
.background(i == highlighted ? Color.gray : Color.clear)
}
}
}
}
Found a way.
Create a function to set a colour if the current index and the scrollTo target are equal.
func highlight(index: Int, target: Int) -> Color {
if target == index {
return .black
} else {
return .clear
}
Then use it to set background, border etc.
Text("Line \(i)")
.border(highlight(index: i, target: 60), width: 5)