英文:
Scroll list loaded from json on load
问题
在你的代码中,你想要将 ScrollView 滚动到列表中的特定索引位置(索引为30),但无论是使用 onAppear
还是 onChange
都没有奏效。以下是可能的解决方法:
- 使用
onAppear
修复滚动:
.onAppear {
withAnimation {
proxy.scrollTo(30)
}
}
- 确保在
ScrollView
中正确设置了标识符(ID),以便在滚动时能够识别和滚动到正确的视图。
ScrollView {
ScrollViewReader { proxy in
ForEach(presenter.countries, id: \.self) { country in
HStack(spacing: 10) {
Text(country.emoji ?? "")
Text(country.phone)
Text(country.name)
Spacer()
}
.id(country.id) // 设置一个唯一的ID
}
.onChange(of: presenter.countries.count, perform: { _ in
withAnimation {
proxy.scrollTo(30, anchor: .top)
}
})
}
}
确保 CountryInfoValue
类型中有一个属性 id
,用于标识每个列表项的唯一性。
这些改进应该有助于你在 ScrollView 中滚动到指定的索引位置。
英文:
I am new to SwiftUI and trying to load a list from a json file which is locally saved. I am able display the json list but I want to scroll it to a specific index in the list.
Following is my code in View:
struct SignUpEnterPhoneNumberView: View {
@StateObject private var presenter = CountryListPresenter()
@State private var searchText = ""
var body: some View {
VStack {
NavigationStack {
ScrollView {
ScrollViewReader { proxy in
ForEach(presenter.countries, id: \.self) { country in
HStack(spacing: 10) {
Text(country.emoji ?? "")
Text(country.phone)
Text(country.name)
Spacer()
}
}
.onChange(of: presenter.countries.count, perform: { _ in
proxy.scrollTo(30)
})
}
}
.navigationBarTitle(Text("Select Country Code"), displayMode: .inline)
Spacer()
}
.searchable(text: $searchText, prompt: presenter.searchTitle)
}
}
}
My presenter file:
class CountryListPresenter: ObservableObject {
let navigationTitle: String
let searchTitle: String
var countries = [CountryInfoValue]()
var selectedIndex = 50
init(
) {
navigationTitle = "Select Country Code"
searchTitle = "Search"
fetchCountryList()
}
func fetchCountryList() {
guard let url = Bundle.main.url(forResource: "countries.emoji", withExtension: "json") else {
return
}
let data = try? Data(contentsOf: url)
if let data = data {
let countryInfo = try? JSONDecoder().decode(CountryInfo.self, from: data)
var countryList = countryInfo?.map { $0.value }
countryList = countryList?.sorted(by: { $0.name < $1.name })
guard let countryList = countryList else {
return
}
countries = countryList
}
}
}
I have tried both onAppear and onChange but nothing works for me.
答案1
得分: 0
请尝试将此代码声明为:
@Published var countries = [CountryInfoValue]()
如果您希望监听 ObservableObject 类的字段更改,请将它们声明为 @Published,以便它们在值更改时能够更新监听器。
我认为,由于它没有被发布,其空状态会被复制到状态中。因此,您无法滚动到项目,因为它是空的。
下面的代码按预期工作。我尝试在那里复制了您的逻辑。
struct ContentView: View {
@StateObject var viewModel = SomeViewMode()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
ForEach($viewModel.items.indices, id: \.self) { index in
ScrollItem()
.onAppear {
proxy.scrollTo(30)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ScrollItem: View {
var body: some View {
Rectangle()
.fill(Color.red)
.frame(minHeight: 300)
}
}
class SomeViewMode: ObservableObject {
@Published var items = [Int]()
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
self?.items = Array(repeating: Int.random(in: 0..<1000), count: 50)
}
}
}
英文:
Please try to declare this as
@Published var countries = [CountryInfoValue]()
If you want to listen to changes of field of ObservableObject class you should declare them @Published so they can update listeners when its value changed.
I believe since its not published its empty state is copied to state. And you cant scrollTo item because its empty.
Code below works as intented. I tried to copy your logic there.
struct ContentView: View {
@StateObject var viewModel = SomeViewMode()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
ForEach($viewModel.items.indices,id:\.self) { index in
ScrollItem()
.onAppear {
proxy.scrollTo(30)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ScrollItem: View {
var body: some View {
Rectangle()
.fill(Color.red)
.frame(minHeight: 300)
}
}
class SomeViewMode : ObservableObject {
@Published var items = [Int]()
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
self?.items = Array(repeating:Int.random(in: 0..<1000), count: 50)
}
}
}
答案2
得分: 0
尝试以下方法,将ScrollViewReader
放在ScrollView
之外,并将.onChange(...)
也放在ScrollView
之外,就像示例代码中所示。
另外,正如我在评论中所说,使用@Published var countries = [CountryInfoValue]()
在class CountryListPresenter: ObservableObject
中。还要声明你的struct CountryInfoValue: Identifiable
,并且不要在ForEach循环中使用, id: \.self
。
以下示例对我有效:
struct ContentView: View {
var body: some View {
SignUpEnterPhoneNumberView()
}
}
struct SignUpEnterPhoneNumberView: View {
@StateObject private var presenter = CountryListPresenter()
@State private var searchText = ""
var body: some View {
VStack {
NavigationStack {
// 用于测试
Button {
// 触发onChange
presenter.countries.append(CountryInfoValue(id: presenter.countries.count))
} label: {
Text("前往30")
}
ScrollViewReader { proxy in
ScrollView {
ForEach(presenter.countries) { country in // <--- 这里,不需要 id: \.self
HStack(spacing: 10) {
Text("\(country.id)")
Text(country.emoji ?? "")
Text(country.phone)
Text(country.name)
Spacer()
}
}
}
.onChange(of: presenter.countries.count) { _ in
withAnimation {
proxy.scrollTo(30) // <--- 这里
}
}
.navigationBarTitle(Text("选择国家代码"), displayMode: .inline)
Spacer()
}
.searchable(text: $searchText, prompt: presenter.searchTitle)
}
}
}
}
struct CountryInfoValue: Identifiable, Hashable { // <-- 这里,Identifiable
let id: Int
var emoji: String? = String(UUID().uuidString.prefix(4))
var phone = String(UUID().uuidString.prefix(5))
var name = String(UUID().uuidString.prefix(6))
}
class CountryListPresenter: ObservableObject {
let navigationTitle: String
let searchTitle: String
@Published var countries = [CountryInfoValue]() // <-- 这里
var selectedIndex = 50
init() {
navigationTitle = "选择国家代码"
searchTitle = "搜索"
fetchCountryList()
}
func fetchCountryList() {
// 用于测试
for index in 0...40 {
countries.append(CountryInfoValue(id: index))
}
}
}
EDIT-1:
你可以在视图加载时使用.onAppear{...}
轻松地"滚动到行",而不是使用Button
。以下是对我有效的代码:
struct SignUpEnterPhoneNumberView: View {
@StateObject private var presenter = CountryListPresenter()
@State private var searchText = ""
var body: some View {
VStack {
NavigationStack {
ScrollViewReader { proxy in
ScrollView {
ForEach(presenter.countries) { country in
HStack(spacing: 10) {
Text("\(country.id)")
Text(country.emoji ?? "")
Text(country.phone)
Text(country.name)
Spacer()
}
}
}
.onAppear {
withAnimation {
// 也可以使用 `.debounce(...)` 进行调整
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(30)
}
}
}
.navigationBarTitle(Text("选择国家代码"), displayMode: .inline)
Spacer()
}
.searchable(text: $searchText, prompt: presenter.searchTitle)
}
}
}
}
英文:
Try this approach, using ScrollViewReader
outside the ScrollView
,
and .onChange(...)
outside the ScrollView
, as shown in the example code.
Also as I said in my comment, use @Published var countries = [CountryInfoValue]()
in your class CountryListPresenter: ObservableObject
. Note also, declare your
struct CountryInfoValue: Identifiable
and do not use the ,id: \.self
in the ForEach loop.
The following example works for me:
struct ContentView: View {
var body: some View {
SignUpEnterPhoneNumberView()
}
}
struct SignUpEnterPhoneNumberView: View {
@StateObject private var presenter = CountryListPresenter()
@State private var searchText = ""
var body: some View {
VStack {
NavigationStack {
// for testing
Button {
// to trigger onChange
presenter.countries.append(CountryInfoValue(id: presenter.countries.count))
} label: {
Text("go to 30")
}
ScrollViewReader { proxy in
ScrollView {
ForEach(presenter.countries) { country in // <--- here, no id: \.self
HStack(spacing: 10) {
Text("\(country.id)")
Text(country.emoji ?? "")
Text(country.phone)
Text(country.name)
Spacer()
}
}
}
.onChange(of: presenter.countries.count) { _ in
withAnimation {
proxy.scrollTo(30) // <--- here
}
}
.navigationBarTitle(Text("Select Country Code"), displayMode: .inline)
Spacer()
}
.searchable(text: $searchText, prompt: presenter.searchTitle)
}
}
}
}
struct CountryInfoValue: Identifiable, Hashable { // <-- here, Identifiable
let id: Int
var emoji: String? = String(UUID().uuidString.prefix(4))
var phone = String(UUID().uuidString.prefix(5))
var name = String(UUID().uuidString.prefix(6))
}
class CountryListPresenter: ObservableObject {
let navigationTitle: String
let searchTitle: String
@Published var countries = [CountryInfoValue]() // <-- here
var selectedIndex = 50
init() {
navigationTitle = "Select Country Code"
searchTitle = "Search"
fetchCountryList()
}
func fetchCountryList() {
// for testing
for index in 0...40 {
countries.append(CountryInfoValue(id: index))
}
}
}
EDIT-1:
you can easily ...scroll to a row on view load
using a .onAppear{...}
instead of a Button
.
Here is the code that works for me:
struct SignUpEnterPhoneNumberView: View {
@StateObject private var presenter = CountryListPresenter()
@State private var searchText = ""
var body: some View {
VStack {
NavigationStack {
ScrollViewReader { proxy in
ScrollView {
ForEach(presenter.countries) { country in
HStack(spacing: 10) {
Text("\(country.id)")
Text(country.emoji ?? "")
Text(country.phone)
Text(country.name)
Spacer()
}
}
}
.onAppear {
withAnimation {
// can also look into `.debounce(...)`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(30)
}
}
}
.navigationBarTitle(Text("Select Country Code"), displayMode: .inline)
Spacer()
}
.searchable(text: $searchText, prompt: presenter.searchTitle)
}
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论