This code was previously working in Xcode11 Beta 4. In the latest Beta 5 I'm getting an error on the ".identified(by:)" block of code.
I looked through the release notes on XCode11 Beta 5 but I didn't see any reference to .identified(by:) being deprecated.
import SwiftUI
import Combine
struct Popups: Decodable {
let name, truckRating, upcomingLocation, cuisine, truckImage, region,
city, state, currentLocation, numberOfRatings, truckExpense : String
}
class NetworkManager: ObservableObject {
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
var popups = [Popups]() {
didSet {
objectWillChange.send(self)
}
}
init() {
guard let url = URL(string:
"https://www.example.com/db.json") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
let popups = try! JSONDecoder().decode([Popups].self, from: data)
DispatchQueue.main.async {
self.popups = popups
}
print("Completed fetching JSON")
}.resume()
}
}
struct ItemsView: View {
#State var networkManager = NetworkManager()
var body: some View {
NavigationView {
List (
networkManager.popups.identified(by: \.name)
) { popup in
ItemsRowView(popup: popup)
}.navigationBarTitle(Text("Pop Ups"))
}
}
}
The error message states "Value of type '[Popups]' has no member 'identified'"
.identified(by:) is deprecated. As you correctly stated, this is not noted in the release notes for Xcode beta, but in the release notes for iOS beta, which is why you couldn't find it. It's a little confusing because the changes relating to SwiftUI are scattered across the release notes for iOS 13 beta, Xcode 11 beta, and macOS Catalina beta.
https://developer.apple.com/documentation/ios_ipados_release_notes/ios_ipados_13_beta_5_release_notes
The identified(by:) method on the Collection protocol is deprecated in
favor of dedicated init(:id:selection:rowContent:) and
init(:id:content:) initializers. (52976883, 52029393)
But the identified(by:) deprecation happened in beta 4, so the following also applies:
SwiftUI APIs deprecated in previous betas are now removed. (52587863)
This question is sort of a duplicate of SwiftUI ForEach 'identified(by:)' is deprecated. Use ForEach(_:id:) or List(_:id:), but the confusion around where the deprecation is mentioned in the release notes merits keeping it as a separate question.
Related
I did a small sample application to show my problem. I used the multi-platform document app template that Xcode 14.0.1 offers, creating my own package file format for this.
I want to create a document based app running on macOS and on iPad.
When running on macOS, everything works as expected.
On the iPad, when opening the app, the file chooser opens.
On opening an existing or creating a new file, the screen looks like this:
The left chevron does nothing, while the right chevron shows the document chooser again.
What's the left, ever so slightly larger chevron on the left doing here and how can I get of it? Is this an error with the framework that should be reported to Apple?
PS don't get distracted by the name of this sample app–the real app will need some navigation and I first thought the 2nd chevron show up cause of this–in the sample I built for this post, there is no navigation though. So this 2nd chevron seems to be a "built in" issue...
For the sake of completeness, here's my code:
import SwiftUI
#main
struct so_DocumentAppWithNavigationShowsMultipleChevronsApp: App {
var body: some Scene {
DocumentGroup(newDocument: so_DocumentAppWithNavigationShowsMultipleChevronsDocument()) { file in
ContentView(document: file.$document)
}
}
}
import UniformTypeIdentifiers
extension UTType {
static var appfroschFile: UTType {
UTType(importedAs: "ch.appfros.so-DocumentAppWithNavigationShowsMultipleChevrons")
}
}
struct so_DocumentAppWithNavigationShowsMultipleChevronsDocument: FileDocument {
var document: Document
init(document: Document = Document(text: "Hello, world!")) {
self.document = document
}
static var readableContentTypes: [UTType] { [.appfroschFile] }
init(configuration: ReadConfiguration) throws {
guard let fileWrappers = configuration.file.fileWrappers
else {
throw CocoaError(.fileReadCorruptFile)
}
guard let documentFileWrapper = fileWrappers["document"],
let data = documentFileWrapper.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
document = try JSONDecoder().decode(Document.self, from: data)
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(document)
let documentFileWrapper = FileWrapper(regularFileWithContents: data)
let mainFileWrapper = FileWrapper(directoryWithFileWrappers: [
"document": documentFileWrapper
])
return mainFileWrapper
}
}
struct Document: Codable {
var text: String
}
struct ContentView: View {
#Binding var document: so_DocumentAppWithNavigationShowsMultipleChevronsDocument
var body: some View {
TextEditor(text: $document.document.text)
}
}
Can see the same problem with the default Document based app when using Xcode 14.1 (14B47) running on the iPad simulator with iOS 16.1. So definitely a bug (and worth reporting to A as such).
At a guess, the second, non-functional back button is what would have been the back button for navigating in SideBar. And the logic to not display when working on a document is what has been broken.
Fortunately simple workaround for the bug is to explicitly specify toolbar's role using the toolbarRole modifier, e.g.
#main
struct so_DocumentAppWithNavigationShowsMultipleChevronsApp: App {
var body: some Scene {
DocumentGroup(newDocument: so_DocumentAppWithNavigationShowsMultipleChevronsDocument()) { file in
ContentView(document: file.$document)
.toolbarRole(.navigationStack) // <-- Specifying this gets rid of double chevron on iOS
}
}
}
I have an app that lists Events from Core Data. Each event has a date.
When I list the Events, I want show the date, unless the date was today or yesterday, and in that case I want to show Today or Yesterday instead of the date.
As of now, I have a function that handles generating the String to show in the row. However, I've noticed that if a day passes and I re-open the app, it shows outdated information. For example, if there is an event from the previous day that said Today when I had the app open the previous day, it will still say Today instead of Yesterday when I re-open the app. Obviously this function is not being called every time I open the app, but I am wondering what the best approach is for making this more dynamic.
These are the avenues I am considering, but not sure what would be best, so I wanted to post here to get recommendations and see if I'm overlooking anything important:
Somehow do something with .onAppear on the row to re-calculate it every time the app is opened (I'm not sure how expensive this date calculation stuff is for each event, but even if it's not expensive I'm not sure how I would tell the rows to re-run the function when the app comes to the foreground)
Switch to a computed property (I don't know if this would be any different than putting a function in there, like I have now. This could be bad to have it called every time if it's an expensive call, but assuming it's not how would I get this to refresh every time the app comes to the foreground?)
Come up with a solution to only re-calculate each row if the day has changed (this is probably what I'd try to do if I knew the row calculation was very expensive, but seems like it might be overkill here, and I'm also not sure how I would go about telling each row to re-run the function)
Here is my code (I left out my date formatter code, but it's pretty standard and shouldn't matter for this):
struct ContentView: View {
#FetchRequest(fetchRequest: Event.eventsNewestFirst)
private var events: FetchedResults<Event>
var body: some View {
NavigationView {
ForEach(events){ event in
EventRow(event: event)
}
}
}
}
struct EventRow: View {
#ObservedObject var event: Event
var body: some View {
Text(event.dateAndTimeString())
}
}
extension Event {
func dateAndTimeString() -> String {
guard let date = self.date else { return "Error" }
let timeString = DateAndNumberFormatters.simpleTimeDisplay.string(from: date)
let dateString: String
if let todayOrYesterday = date.asTodayOrYesterday() {
dateString = todayOrYesterday
} else {
dateString = DateAndNumberFormatters.simpleShortDateDisplay.string(from: date)
}
return "\(dateString) at \(timeString)"
}
}
extension Date {
func asTodayOrYesterday() -> String? {
let calendar = Calendar.current
let dayComponents = calendar.dateComponents([.year, .month, .day], from: self)
let todayDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
var yesterdayDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
yesterdayDateComponents.day = yesterdayDateComponents.day! - 1
let dayDate: Date! = calendar.date(from: dayComponents)
let todayDayDate: Date! = calendar.date(from: todayDateComponents)
let yesterdayDayDate: Date! = calendar.date(from: yesterdayDateComponents)
switch dayDate {
case todayDayDate:
return "Today"
case yesterdayDayDate:
return "Yesterday"
default:
return nil
}
}
}
The possible approach is to observe scene phase and force refresh observed core data object as needed, like
struct EventRow: View {
#ObservedObject var event: Event
#Environment(\.scenePhase) var scenePhase
var body: some View {
Text(event.dateAndTimeString())
.onChange(of: scenePhase) {
if $0 == .active {
event.objectWillChange.send()
}
}
}
}
The scenePhase approach in another answer did not work.
The solution I ended up using relies on a publisher of UIApplication.didBecomeActiveNotification instead:
struct EventRow: View {
#ObservedObject var event: Event
#State private var dateAndTime: String = "Error"
var body: some View {
NavigationLink(destination: EventDetailView(event: event)) {
Text(dateAndTime)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
dateAndTime = event.dateAndTimeString()
}
}
}
}
I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String
Edit (original text below could be ignored)
I did some further testing using only a manual fetch (which gets executed only once). It seems it's just the fetch request (for only 12000 entities) itself which is horribly slow. Nevertheless, I wonder why I should get this issue only for macOS and not iOS given it's the same code?
==========================================
Original post
I am developing a simple SwiftUI app for macOS Big Sur/iOS 14 which synchronizes "Card"-entities (nothing fancy; around 20 properties and 7 relationships) via CloudKit. Everything works fine, however, after importing (only) 12000 entries (from a decoded JSON file), the macOS app (but not the iOS app) became unusable, i.e. freezes/has a very high CPU usage. Strangely, this issue also occurs if no data is shown on the UI, e.g. with the following simple view:
MainView & CardList
struct MainView: View {
#ObservedObject var model: MainViewModel
#Environment(\.colorScheme) var colorScheme
var body: some View {
CardList(predicate: Card.predicate(searchText: self.model.searchText,
pinned: self.model.filterPinned,
priority: self.model.getPrioritiesAsArray(),
subjects: self.model.selectedSubjects,
bundles: self.model.selectedBundles),
sortDescriptor: EntrySort(sortType: self.model.sortType, sortOrder:self.model.sortOrder).sortDescriptor,
model: self.model)
}
}
struct CardList: View {
#ObservedObject var model: MainViewModel
#Environment(\.colorScheme) var colorScheme
#Environment(\.managedObjectContext)
var context: NSManagedObjectContext
#FetchRequest(
entity: Card.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Card.id, ascending: false)
]
)
private var result: FetchedResults<Card>
init(predicate: NSPredicate?,
sortDescriptor: NSSortDescriptor,
model: MainViewModel) {
let fetchRequest = NSFetchRequest<Card>(entityName: "Card")
fetchRequest.sortDescriptors = [sortDescriptor]
if let predicate = predicate {
fetchRequest.predicate = predicate
}
_result = FetchRequest(fetchRequest: fetchRequest)
self.model = model
}
var body: some View {
Text("TEST")
}
}
The "MainViewModel" look as follows (abbreviated for better legibility):
MainViewModel
class MainViewModel: ObservableObject {
#Published var searchText: String = ""
#Published var selectedSubjects: [Subject]?
#Published var selectedBundles: [Bundle]?
#Published var selectedTags: [Tag]?
#Published var filterPinned: Bool? = nil
#Published var filterPriorityVeryHigh: Bool = false
#Published var filterPriorityHigh: Bool = false
#Published var filterPriorityNormal: Bool = false
#Published var filterPriorityLow: Bool = false
func getPrioritiesAsArray() -> [CardPriority] {
var res: [CardPriority] = []
if self.filterPriorityLow { res.append(CardPriority.Low)}
if self.filterPriorityNormal { res.append(CardPriority.Normal)}
if self.filterPriorityHigh { res.append(CardPriority.High)}
if self.filterPriorityVeryHigh { res.append(CardPriority.VeryHigh)}
return res
}
#Published var sortType = SortType.dateCreated
#Published var sortOrder = SortOrder.ascending
}
AppDelegate
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var coreDataStack = CoreDataStack(modelName: "[Name of Model]")
var mainViewModel = MainViewModel()
func applicationDidFinishLaunching(_ aNotification: Notification) {
coreDataStack.viewContext.automaticallyMergesChangesFromParent = true
let mainView = MainView(model: mainViewModel)
.environment(\.managedObjectContext, coreDataStack.viewContext)
// ...
}
Question
The very same data was successfully synchronized to the iOS version of the app and can be displayed there without any performance issues (using the same controls, namely a LazyVStack). However, the macOS app became totally unusable because of the constant high CPU usage. Also, I cannot see anything that would constantly change a variable and thereby trigger a constant refresh of the displayed entities (i.e. trigger a new fetch request).
Does anybody have an idea why this issue occurs?
Thanks in advance for any advise!
Sebastian
※ One thing I noticed is that Xcode floods the console with the warning "NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release" because I am still using some [String]-Transformable-Properties on the Card entity. I will address this issue later, but since this warning seems to be displayed constantly, I wonder if #FetchRequest constantly fetches/evaluates all entities? Further, I assume the vast amount of warnings displayed should only have an influence on Xcode's CPU usage?
I solved this issue; it seems that it was indeed the warning "'NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release" which caused this spike in CPU usage. After setting "NSSecureUnarchiveFromDataTransformer" to all [String]-Transformable-Properties of all database entities, the issue was gone.
So I'm retrieving data from FireStore. I'm retrieving the data successfully. When I tap my search button the first time the data is being downloaded and the new view is pushed. As a result, I get a blank view. But when I go back, hit search again, sure enough I can see my data being presented.
How can I make sure I first have the data I'm searching for THEN navigate to the new view? I've used #State variables etc. But nothing seems to be working. I am using the MVVM approach.
My ViewModel:
class SearchPostsViewModel: ObservableObject {
var post: [PostModel] = []
#State var searchCompleted: Bool = false
func searchPosts(completed: #escaping() -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
isLoading = true
API.Post.searchHousesForSale(propertyStatus: propertyStatus, propertyType: propertyType, location: location, noOfBathrooms: noOfBathroomsValue, noOfBedrooms: noOfBedroomsValue, price: Int(price!)) { (post) in
self.post = post
print(self.post.count)
self.isLoading = false
self.searchCompleted.toggle()
}
}
}
The code that does work, but with the bug:
NavigationLink(destination: FilterSearchResults(searchViewModel: self.searchPostsViewModel)
.onAppear(perform: {
DispatchQueue.main.async {
self.createUserRequest()
}
})
)
{
Text("Search").modifier(UploadButtonModifier())
}
Try with the following modified view model
class SearchPostsViewModel: ObservableObject {
#Published var post: [PostModel] = [] // << make both published
#Published var searchCompleted: Bool = false
func searchPosts(completed: #escaping() -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
isLoading = true
API.Post.searchHousesForSale(propertyStatus: propertyStatus, propertyType: propertyType, location: location, noOfBathrooms: noOfBathroomsValue, noOfBedrooms: noOfBedroomsValue, price: Int(price!)) { (post) in
DispatchQueue.main.async {
self.post = post // << update on main queue
print(self.post.count)
self.isLoading = false
self.searchCompleted.toggle()
}
}
}
}
You should look at the Apple documentation for #State and ObservableObject
https://developer.apple.com/documentation/combine/observableobject
https://developer.apple.com/documentation/swiftui/state
Your issue is with using an #State in a non-UI class/View.
It might help if you start with the Apple SwiftUI tutorials. So you understand the differences in with the wrappers and learn how it all connects.
https://developer.apple.com/tutorials/swiftui
Also, when you post questions make sure your code can be copied and pasted onto Xcode as-is so people can test it. You will get better feedback if other developers can see what is actually happening. As you progress it won't be as easy to see issues.