最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - Model function breaks Pinned Headers - Stack Overflow

programmeradmin1浏览0评论

I have a basic SwiftUI view that shows items releasing and they are grouped by date, each date has its own header that gets pinned and its releases. When the view initially appears the pinned headers work, however if I call the Model function getReleases() then the pinnedHeaders stop working as shown in the video below. I have tried updating State values after the function is called to allow the view to update, I also tried putting the function in a background thread. Note the service function does a basic Firebase query. Also the bug occurs even if the firebase query service function returns NO documents.

Model

import Foundation

@Observable class FeedModel {
    var releases = [ReleaseHolder]()
    var lastUpdatedReleases: Date? = nil
    var gotReleases = false

    func getReleases() {
        DispatchQueue.main.async {
            self.lastUpdatedReleases = Date()
        }
        
        DispatchQueue.global(qos: .background).async {
            ReleaseService().GetNewReleases { data in
                let calendar = Calendar.current
                let formatter = DateFormatter()
                
                formatter.dateFormat = "EEEE, MMMM d"
                
                let groupedReleases = Dictionary(grouping: data) { release -> String in
                    let date = release.releaseTime.dateValue()
                    return formatter.string(from: date)
                }
                
                var releaseHolders = groupedReleases.map { (dateString, releases) in
                    let firstReleaseDate = releases.first?.releaseTime.dateValue() ?? Date()
                    
                    let adjustedDateString: String
                    if calendar.isDateInToday(firstReleaseDate) {
                        adjustedDateString = "Today"
                    } else if calendar.isDateInTomorrow(firstReleaseDate) {
                        adjustedDateString = "Tomorrow"
                    } else {
                        adjustedDateString = formatter.string(from: firstReleaseDate)
                    }
                    
                    let sortedReleases = releases.sorted {
                        $0.releaseTime.dateValue() < $1.releaseTime.dateValue()
                    }
                    
                    return ReleaseHolder(dateString: adjustedDateString, releases: sortedReleases)
                }
                
                releaseHolders.sort {
                    let date1 = $0.releases.first?.releaseTime.dateValue() ?? Date.distantFuture
                    let date2 = $1.releases.first?.releaseTime.dateValue() ?? Date.distantFuture
                    return date1 < date2
                }
                
                DispatchQueue.main.async {
                    self.releases = releaseHolders
                    self.gotReleases = true
                }
            }
        }
    }
}

View

import SwiftUI

struct HomeFeedView: View {
    @Environment(FeedModel.self) private var model
    @Environment(\.colorScheme) var colorScheme
   
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(spacing: 10, pinnedViews: [.sectionHeaders]){
                    Color.clear.frame(height: 1).id("scrolltop")
                    
                    let data = getData()
                    
                    if data.isEmpty {
                         VStack(spacing: 12){
                             Text("Nothing yet...")
                         }
                    } else {
                        ForEach(data) { holder in
                            Section {
                                ForEach(holder.releases) { release in
                                    NavigationLink {
                                        ReleaseView(release: release)
                                    } label: {                                        
                                        FeedRowView()
                                    }
                                }
                            } header: {
                                HStack(spacing: 6){
                                    Text(holder.dateString).font(.headline).bold()
                                    Spacer()
                                }
                            }
                        }
                    }
                    
                    Color.clear.frame(height: 120)
                }
            }
            .safeAreaPadding(.top, 60 + top_Inset())
            .refreshable {
                model.getReleases()
            }
        }
        .overlay(alignment: .top) {
            headerView()
        }
        .ignoresSafeArea()
        .onAppear(perform: {
            model.getReleases()
        })
    }
    func getData() -> [ReleaseHolder] {
        if filter == "Past Drops" {
            return model.pastReleases
        }
        if filter == "No filter" {
            return model.releases
        }
        
        var final = [ReleaseHolder]()
        
        model.releases.forEach { element in
            var new = ReleaseHolder(dateString: element.dateString, releases: [])
            var newPosts = [Release]()
            
            element.releases.forEach { single in
                if (filter == "Sneakers" && single.type == 4) || (filter == "Apparel" && single.type == 3) || (filter == "Tickets" && single.type == 2) || (filter == "Collectibles" && single.type == 1) || (filter == "Electronics" && single.type == 5) {
                    newPosts.append(single)
                }
            }
            
            if !newPosts.isEmpty {
                new.releases = newPosts
                final.append(new)
            }
        }
        
        return final
    }
    @ViewBuilder
    func headerView() -> some View {
        ZStack {
            HStack {
                ZStack(alignment: .bottomTrailing){
                    NavigationLink {
                        ProfileView()
                    } label: {
                        ZStack {
                            Image(systemName: "person.crop.circle.fill")
                                .resizable()
                                .foregroundStyle(.gray)
                                .frame(width: 42, height: 42)
                                .clipShape(Circle())
                        }
                    }
                }
                Spacer()
            }
        }
    }
}

I have a basic SwiftUI view that shows items releasing and they are grouped by date, each date has its own header that gets pinned and its releases. When the view initially appears the pinned headers work, however if I call the Model function getReleases() then the pinnedHeaders stop working as shown in the video below. I have tried updating State values after the function is called to allow the view to update, I also tried putting the function in a background thread. Note the service function does a basic Firebase query. Also the bug occurs even if the firebase query service function returns NO documents.

https://drive.google/file/d/1ZUoMF60jkXl5RigOAFp0SdnpJVIohRwj/view?usp=sharing

Model

import Foundation

@Observable class FeedModel {
    var releases = [ReleaseHolder]()
    var lastUpdatedReleases: Date? = nil
    var gotReleases = false

    func getReleases() {
        DispatchQueue.main.async {
            self.lastUpdatedReleases = Date()
        }
        
        DispatchQueue.global(qos: .background).async {
            ReleaseService().GetNewReleases { data in
                let calendar = Calendar.current
                let formatter = DateFormatter()
                
                formatter.dateFormat = "EEEE, MMMM d"
                
                let groupedReleases = Dictionary(grouping: data) { release -> String in
                    let date = release.releaseTime.dateValue()
                    return formatter.string(from: date)
                }
                
                var releaseHolders = groupedReleases.map { (dateString, releases) in
                    let firstReleaseDate = releases.first?.releaseTime.dateValue() ?? Date()
                    
                    let adjustedDateString: String
                    if calendar.isDateInToday(firstReleaseDate) {
                        adjustedDateString = "Today"
                    } else if calendar.isDateInTomorrow(firstReleaseDate) {
                        adjustedDateString = "Tomorrow"
                    } else {
                        adjustedDateString = formatter.string(from: firstReleaseDate)
                    }
                    
                    let sortedReleases = releases.sorted {
                        $0.releaseTime.dateValue() < $1.releaseTime.dateValue()
                    }
                    
                    return ReleaseHolder(dateString: adjustedDateString, releases: sortedReleases)
                }
                
                releaseHolders.sort {
                    let date1 = $0.releases.first?.releaseTime.dateValue() ?? Date.distantFuture
                    let date2 = $1.releases.first?.releaseTime.dateValue() ?? Date.distantFuture
                    return date1 < date2
                }
                
                DispatchQueue.main.async {
                    self.releases = releaseHolders
                    self.gotReleases = true
                }
            }
        }
    }
}

View

import SwiftUI

struct HomeFeedView: View {
    @Environment(FeedModel.self) private var model
    @Environment(\.colorScheme) var colorScheme
   
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(spacing: 10, pinnedViews: [.sectionHeaders]){
                    Color.clear.frame(height: 1).id("scrolltop")
                    
                    let data = getData()
                    
                    if data.isEmpty {
                         VStack(spacing: 12){
                             Text("Nothing yet...")
                         }
                    } else {
                        ForEach(data) { holder in
                            Section {
                                ForEach(holder.releases) { release in
                                    NavigationLink {
                                        ReleaseView(release: release)
                                    } label: {                                        
                                        FeedRowView()
                                    }
                                }
                            } header: {
                                HStack(spacing: 6){
                                    Text(holder.dateString).font(.headline).bold()
                                    Spacer()
                                }
                            }
                        }
                    }
                    
                    Color.clear.frame(height: 120)
                }
            }
            .safeAreaPadding(.top, 60 + top_Inset())
            .refreshable {
                model.getReleases()
            }
        }
        .overlay(alignment: .top) {
            headerView()
        }
        .ignoresSafeArea()
        .onAppear(perform: {
            model.getReleases()
        })
    }
    func getData() -> [ReleaseHolder] {
        if filter == "Past Drops" {
            return model.pastReleases
        }
        if filter == "No filter" {
            return model.releases
        }
        
        var final = [ReleaseHolder]()
        
        model.releases.forEach { element in
            var new = ReleaseHolder(dateString: element.dateString, releases: [])
            var newPosts = [Release]()
            
            element.releases.forEach { single in
                if (filter == "Sneakers" && single.type == 4) || (filter == "Apparel" && single.type == 3) || (filter == "Tickets" && single.type == 2) || (filter == "Collectibles" && single.type == 1) || (filter == "Electronics" && single.type == 5) {
                    newPosts.append(single)
                }
            }
            
            if !newPosts.isEmpty {
                new.releases = newPosts
                final.append(new)
            }
        }
        
        return final
    }
    @ViewBuilder
    func headerView() -> some View {
        ZStack {
            HStack {
                ZStack(alignment: .bottomTrailing){
                    NavigationLink {
                        ProfileView()
                    } label: {
                        ZStack {
                            Image(systemName: "person.crop.circle.fill")
                                .resizable()
                                .foregroundStyle(.gray)
                                .frame(width: 42, height: 42)
                                .clipShape(Circle())
                        }
                    }
                }
                Spacer()
            }
        }
    }
}
Share Improve this question edited Feb 4 at 10:01 malhal 30.9k7 gold badges123 silver badges150 bronze badges asked Feb 4 at 7:09 Ahmed ZaidanAhmed Zaidan 711 gold badge9 silver badges24 bronze badges 4
  • Your code is not reproducible so we can't test an actual solution. But I don't see any @State in your HomeFeedView, so how can the view react to updates? You can't expect @Environment to act as a state when you make environment calls and expect changes from the same view. You can try adding something like @State private var data : [ReleaseHolder] = [], use .onAppear to populate it: data = getData() and then in .refreshable, update the model and then the state: model.getReleases(); self.data = model.releases. And remove the let data = getData() from the body. – Andrei G. Commented Feb 9 at 18:54
  • @AndreiG. I dont think the issue is with the view updating. Even when no new documents are read the bug happens. Also I tried your approach of storing the releases as a state variable. And then my viewModel function uses a completion to pass the array to the view, which is then set to the state variable. The same issue happens with this approach too – Ahmed Zaidan Commented Feb 10 at 3:51
  • You really should take the time to put together some reproducible code. The process of it may also help you identify the issue. The sample code doesn't have to use the same api calls and what not, look into creating some mock data previews. – Andrei G. Commented Feb 10 at 18:09
  • The issue here is that the view state loses sync with the actual data. Now this could be because it doesn't at all react to changes in the model (like when you have to go back and open the view again to see the changed data), or because of a timing issue, which is what I believe happens here due to the async nature of the calls (that lose sync with the view state). I managed to piece it together and get it working, I'll post an answer. – Andrei G. Commented Feb 10 at 21:12
Add a comment  | 

1 Answer 1

Reset to default 1 +50

I managed to piece it together to get it working with some mock data and a mocked interface, since your code was not reproducible.

As I mentioned in the comments, the issue here is likely with the async nature of the functions of the model, which are not in sync with the view state.

I am not familiar with DispatchQueue so the code below uses more current methods using concurrency: async, await and Task.

Basically, you just want to make sure the UI knows to wait for the completion of the calls, in order to update the view state at the right time. You can look at the .onAppear and .refreshable modifiers and maybe the model's getReleases() function on how it's done.

Full code:

import SwiftUI

// Release and ReleaseHolder models
struct Release: Identifiable {
    let id = UUID()
    let type: Int
    let name: String
    let releaseTime: Date
}

struct ReleaseHolder: Identifiable {
    let id = UUID()
    var dateString: String
    var releases: [Release]
}

@Observable class FeedModel {
    var releases = [ReleaseHolder]()
    var lastUpdatedReleases: Date? = nil
    var gotReleases = false
    
    func getReleases() async {
        // Simulate network call using async
        let sampleReleases = await fetchReleases()
        
        let groupedReleases = Dictionary(grouping: sampleReleases) { release -> String in
            let formatter = DateFormatter()
            formatter.dateFormat = "EEEE, MMMM d"
            return formatter.string(from: release.releaseTime)
        }
        
        let releaseHolders = groupedReleases.map { (dateString, releases) in
            ReleaseHolder(dateString: dateString, releases: releases)
        }
        
        // Update the UI on the main thread
        await MainActor.run {
            self.releases = releaseHolders
            self.gotReleases = true
        }
    }
    
    // Helper function simulating fetching data
    func fetchReleases() async -> [Release] {
        // Simulating a network delay
        try? await Task.sleep(nanoseconds: 1_000_000_000) // Sleep for 1 second
        
        let now = Date()
        
        let releases = (1...50).map { i -> Release in
            let releaseType = (i % 5) + 1  // Types 1 to 5
            let releaseName = "Release \(i)"
            let releaseTime = now.addingTimeInterval(Double(i * 86400))  // Sequential days (86400 seconds in a day)
            
            return Release(type: releaseType, name: releaseName, releaseTime: releaseTime)
        }
        
        return releases
    }
}

struct ReleaseHomeFeedView: View {
    @Environment(FeedModel.self) private var model
    @Environment(\.colorScheme) var colorScheme
    
    @State private var typeFilter: Int?
    
    var body: some View {
        NavigationStack {
            ScrollViewReader { proxy in
                ScrollView {
                    LazyVStack(spacing: 10, pinnedViews: [.sectionHeaders]){
                        Color.clear.frame(height: 1).id("scrolltop")
                        
                        let data = getData()
                        
                        if data.isEmpty {
                            VStack(spacing: 12){
                                Text("Nothing yet...")
                            }
                        } else {
                            ForEach(data) { holder in
                                Section {
                                    ForEach(holder.releases) { release in
                                        NavigationLink {
                                            Text(release.name) // Dummy Detail View
                                        } label: {
                                            // Dummy Feed Row
                                            HStack(spacing: 20) {
                                                Rectangle()
                                                    .fill(.blue)
                                                    .aspectRatio(1, contentMode: .fit)
                                                    .frame(width: 100)
                                                    .overlay {
                                                        Image(systemName: "photo.tv")
                                                            .foregroundStyle(.white)
                                                            .imageScale(.large)
                                                    }
                                                VStack(alignment: .leading) {
                                                    Text(release.name)
                                                        .foregroundStyle(.secondary)
                                                    Text("Type: \(release.type)")
                                                        .foregroundStyle(.tertiary)
                                                    Text("Details")
                                                }
                                            }
                                            .frame(maxWidth: .infinity, alignment: .leading)
                                            .background(.regularMaterial )
                                            .clipShape(RoundedRectangle(cornerRadius: 12))
                                            .padding(.leading)
                                        }
                                        .buttonStyle(.plain)
                                    }
                                } header: {
                                    HStack(spacing: 6){
                                        Text(holder.dateString).font(.headline).bold()
                                            .padding(.horizontal)
                                        Spacer()
                                    }
                                    .padding(.vertical)
                                    .background(.thinMaterial)
                                }
                            }
                        }
                    }
                }
                .refreshable {
                    await model.getReleases()
                    withAnimation {
                        proxy.scrollTo("scrolltop", anchor: .top)  // Scroll to top
                    }
                }
            }
            .onAppear {
                Task {
                    await model.getReleases()
                }
            }
            .toolbarTitleDisplayMode(.inline)
            .toolbar {
                
                ToolbarItem(placement: .topBarLeading) {
                    Image(systemName: "person.crop.circle.fill")
                }
                
                ToolbarItem(placement: .principal) {
                    Image(systemName: "crown.fill")
                }
                
                ToolbarItem(placement: .primaryAction) {
                    Button {
                        typeFilter = typeFilter == nil ? 4 : nil
                    } label: {
                        Image(systemName: "line.3.horizontal.decrease")
                            .foregroundStyle(typeFilter == nil ? Color.primary : Color.blue)
                    }
                    .buttonStyle(.plain)
                }
                
                ToolbarItem(placement: .primaryAction) {
                    Image(systemName: "chevron.down")
                }
            }
        }
    }
    
    func getData() -> [ReleaseHolder] {
        
        guard let filter = typeFilter else {
            return model.releases
        }
        
        return model.releases.filter { holder in
            holder.releases.contains { $0.type == filter }
        }
    }
    
    @ViewBuilder
    func headerView() -> some View {
        HStack {
            NavigationLink {
                Text("Profile view")
            } label: {
                Image(systemName: "person.crop.circle.fill")
                    .resizable()
                    .foregroundStyle(.gray)
                    .frame(width: 42, height: 42)
                    .clipShape(Circle())
            }
            Spacer()
            Image(systemName: "crown.fill")
                .font(.system(size: 50))
            Spacer()
        }
        .padding()
        .background(.regularMaterial)
    }
}

#Preview {
    @Previewable @State var model = FeedModel()
        ReleaseHomeFeedView()
            .environment(model)
}

发布评论

评论列表(0)

  1. 暂无评论