滚动列表在加载时从 JSON 加载

huangapple go评论80阅读模式
英文:

Scroll list loaded from json on load

问题

在你的代码中,你想要将 ScrollView 滚动到列表中的特定索引位置(索引为30),但无论是使用 onAppear 还是 onChange 都没有奏效。以下是可能的解决方法:

  1. 使用 onAppear 修复滚动:
.onAppear {
    withAnimation {
        proxy.scrollTo(30)
    }
}
  1. 确保在 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..&lt;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  // &lt;--- 这里,不需要 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)  // &lt;--- 这里
                        }
                    }
                    .navigationBarTitle(Text("选择国家代码"), displayMode: .inline)
                    Spacer()
                }
                .searchable(text: $searchText, prompt: presenter.searchTitle)
            }
        }
    }
}

struct CountryInfoValue: Identifiable, Hashable {  // &lt;-- 这里,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]() // &lt;-- 这里
    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 = &quot;&quot;
    
    var body: some View {
        VStack {
            NavigationStack {
                // for testing
                Button {
                    // to trigger onChange
                    presenter.countries.append(CountryInfoValue(id: presenter.countries.count))
                } label: {
                    Text(&quot;go to 30&quot;)
                }
                ScrollViewReader { proxy in
                    ScrollView {
                        ForEach(presenter.countries) { country in  // &lt;--- here, no id: \.self
                            HStack(spacing: 10) {
                                Text(&quot;\(country.id)&quot;)
                                Text(country.emoji ?? &quot;&quot;)
                                Text(country.phone)
                                Text(country.name)
                                Spacer()
                            }
                        }
                    }
                    .onChange(of: presenter.countries.count) { _ in
                        withAnimation {
                            proxy.scrollTo(30)  // &lt;--- here
                        }
                    }
                    .navigationBarTitle(Text(&quot;Select Country Code&quot;), displayMode: .inline)
                    Spacer()
                }
                .searchable(text: $searchText, prompt: presenter.searchTitle)
            }
        }
    }
}

struct CountryInfoValue: Identifiable, Hashable {  // &lt;-- 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]() // &lt;-- here
    var selectedIndex = 50
    
    init() {
        navigationTitle = &quot;Select Country Code&quot;
        searchTitle = &quot;Search&quot;
        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 = &quot;&quot;

    var body: some View {
        VStack {
            NavigationStack {
                ScrollViewReader { proxy in
                    ScrollView {
                        ForEach(presenter.countries) { country in
                            HStack(spacing: 10) {
                                Text(&quot;\(country.id)&quot;)
                                Text(country.emoji ?? &quot;&quot;)
                                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(&quot;Select Country Code&quot;), displayMode: .inline)
                    Spacer()
                }
                .searchable(text: $searchText, prompt: presenter.searchTitle)
            }
        }
    }
}

huangapple
  • 本文由 发表于 2023年6月27日 19:33:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/76564435.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定