SwiftUI List only using first item of given array - ios

I have a simple view below to display all of the contacts for the user:
struct AllContactsView: View {
static let withState: some View = AllContactsView().environmentObject(AllContactsProvider())
#EnvironmentObject var contactsProvider: AllContactsProvider
let title: UserListType = .selectInvitees
var body: some View {
List(self.contactsProvider.invitees) { invite in
self.row(for: invite)
}
.navigationBarTitle(Text(self.title.rawValue))
.onAppear(perform: self.contactsProvider.fetch)
}
func row(for invite: Invitee) -> some View {
// everything below is only printed out once!
print(self.contactsProvider.invitees.prettyPrinted) // shows correct array of contacts
print(type(of: self.contactsProvider.invitees)) // this is indeed an array
print(invite) // prints out the first item in the array (which is expected on first pass)
return UserRow(invitee: invite)
}
}
I am manipulating the array of CNContacts I get like this to an array of Invitees, which is what I am attempting to display in my list:
self?.invitees = contacts.asSimpleContacts.map({ $0.asUser.asInvitee })
Using the supporting functions and extensions below:
// Contact Value simplified so we can pass it around as a value type.
public struct SimpleContact: Hashable, Codable {
let firstName: String
let lastName: String
let emails: [String]
let phoneNumbers: [PhoneNumber]
var fullName: String { "\(self.firstName) \(self.lastName)" }
var asUser: User {
User(
id: Constants.unsavedID,
username: self.fullName,
picURL: "al",
email: self.emails.first ?? "",
phone: self.phoneNumbers.first ?? "",
created: Date().timeIntervalSince1970
)
}
}
extension CNContact {
/// Returns the `SimpleContact` representation of `self`
var asSimpleContact: SimpleContact {
SimpleContact(
firstName: self.givenName,
lastName: self.familyName,
emails: self.emailAddresses.map({ String($0.value) }),
phoneNumbers: self.phoneNumbers.map({ Authentication.sanitize(phoneNo: $0.value.stringValue) })
)
}
}
extension Array where Element == CNContact {
/// Returns the `SimpleContact` mapping of `self`
var asSimpleContacts: [SimpleContact] { self.map({ $0.asSimpleContact }) }
}
public struct User: Hashable, Codable, Identifiable {
public let id: String
let username: String
let picURL: String
let email: String
let phone: String
let created: Double
var asInvitee: Invitee { Invitee(user: self, isGoing: false) }
}
The contacts are populated into self.contactsProvider.invitees as expected following self.contactsProvider.fetch(). However, SwiftUI is displaying self.contactsProvider.invitees.count instances of self.contactsProvider.invitees.first, rather than each contact. I have compared my approach below to other examples online and can't seem to find where I went wrong. I have determined that the issue lies somewhere with the contacts manipulation - when I supply a mocked array of invitees, everything works as expected, despite things compiling and running as expected without the mocks, and printing and debugging not revealing anything.
Any help would be appreciated.

I just ran into this issue, to answer a bit more clearly:
Each row in a SwiftUI list should be generated with a unique ID.
If you are using the List() function to create the view, make sure you are specifying an id
struct MenuView: View {
var body: some View {
VStack {
Section(header: MenuSectionHeader()) {
//item.sku is the unique identifier for the row
List(Menu.allItems, id:\.sku) { item in
MenuItemRow(item)
}
}
}
}

For anyone who runs across something similar, the problem was that the ID I was instantiating the objects with was not unique. Curious that this would be the expected behavior if that error is made, but thats what it was.

Related

#Published Realm object not triggering view redrawing in SwiftUI

In our app we use a UserService that is a ObservableObject and passed as environment. A synced realm is opened and the app user (a RealmObject) is obtained using flexible sync.
When updating the users properties, such as his username, the view does not get redrawn. This is against my expectations since UserService contains a #Published property where the user (that is being edited) is stored. On the database it clearly shows the property being edited, however the view does not get redrawn, only when restarting the app the new properties are shown.
What would be the best way to have a UserService objects taking care of all user related logic (storing a user object (reference to it), containing functions to update, ...) and use this to display the active data of this user throughout the views?
Here is a MRE (the login logic is left out to reduce complexity):
import SwiftUI
import RealmSwift
class UserService2: ObservableObject {
var realm: Realm
#Published var ownUser: User
var userNotificationToken: NotificationToken?
init(realm: Realm, ownUser: User) {
self.realm = realm
self.ownUser = ownUser
userNotificationToken = ownUser.observe { change in
print(change) // just to see that the user object is actually live and being updated...
}
}
func changeName(newName: String) {
do {
try self.realm.write {
self.ownUser.userName = newName
}
} catch {
print("error")
}
}
}
struct TestView: View {
#EnvironmentObject var userService: UserService2
var body: some View {
VStack {
Text(userService.ownUser.userName ?? "no name")
Button {
userService.changeName(newName: Date().description)
} label: {
Text("change name")
}
}
}
}
struct ContentView: View {
var realm: Realm? = nil
init() {
let flexSyncConfig = app.currentUser!.flexibleSyncConfiguration(initialSubscriptions: { subs in
subs.append(
QuerySubscription<User>(name: "ownUserQuery") {
$0._id == "123"
})
})
do {
let realm = try Realm(configuration: flexSyncConfig)
self.realm = realm
} catch {
print("sth went wrong")
}
}
var body: some View {
if let realm = realm, let ownUser = realm.objects(User.self).where( { $0._id == "123" } ).first {
TestView()
.environmentObject(UserService2(realm: realm, ownUser: ownUser))
} else {
ProgressView()
}
}
}
The User Object looks like this
import Foundation
import RealmSwift
class User: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id = UUID().uuidString
#Persisted var userName: String?
convenience init(_id: String? = nil, userName: String? = nil) {
self.init()
if let _id = _id {
self._id = _id
}
self.userName = userName
}
}
P.S. I assume I could observe changes on the object using realm and somehow force a view refresh, but I would find it much more clean using the already existing way to watch for changes and redraw views when needed using #Published...
P.P.S. This user object is created on the server using a trigger when someone authenticates. However, I assume this is not really relevant to this problem.
The issue here is the usage of a reference type as "Source of truth".
ObservableObject and SwiftUI Views use Combine Publishers to know when to refresh.
The #Published value sends the .objectWillChange publisher of the ObservableObject only when its wrapped value "changes". "changes" in this context means it gets replaced. So value types are preferred here, because if you change one of the properties the whole object will be replaced. This does not happen for reference types.
Multiple possible solutions here:
change the User class to a struct (Probably not wanted here, because this object implements Realm)
use the .objectWillChange.send() method yourself before altering the user
instead of altering the ownUservar replace it with a new one that contains the new information.
func changeName(newName: String) {
do {
self.objectWillChange.send() //add this
try self.realm.write {
self.ownUser.userName = newName
}
} catch {
print("error")
}
}

SwiftUI design pattern - How to make a "2nd-stage" API call from the result of the first one

This is on iOS 15.5 using the latest SwiftUI standards.
I have these two structs in my SwiftUI application:
User.swift
struct User: Codable, Identifiable, Hashable {
let id: String
let name: String
var socialID: String? // it's a var so I can modify it later
func getSocialID() async -> String {
// calls another API to get the socialID using the user's id
// code omitted
// example response:
// {
// id: "aaaa",
// name: "User1",
// social_id: "user_1_social_id",
// }
}
}
Video.swift
struct Video: Codable, Identifiable, Hashable {
let id: String
let title: String
var uploadUser: User
}
My SwiftUI application displays a list of videos, the list of videos are obtained from an API (which I have no control over), the response looks like this:
[
{
id: "AAAA",
title: "My first video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
{
id: "BBBB",
title: "My second video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
]
My video's view model looks like this:
VideoViewModel.swift
#MainActor
class VideoViewModel: ObservableObject {
#Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()
let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = responseResult
}
func getSocialIDForAll() async throws -> [String: String?] {
var socialList: [String: String?] = [:]
try await withThrowingTaskGroup(of: (String, String?).self) { group in
for video in self.videoList {
group.addTask {
return (video.id, try await video.uploadedUser.getSocialId())
}
}
for try await (userId, socialId) in group {
socialList[userId] = socialId
}
}
return socialList
}
}
Now, I wish to fill in the socialID field for the User struct, which I must obtain from another API using each user's ID. the response looks like this for each user:
{
id: "aaaa",
name: "User1",
social_id: "user_1_social_id",
}
Right now the only viable way to get the information seems to be using withThrowingTaskGroup() and call getSocialID() for each user, which I am using right now, then I can return a dictionary that contains all the socialID information for each user, then the dictionary can be used in SwiftUI views.
But, is there a way for me to fill in the socialID field in the User struct without having to use a separate dictionary? It doesn't seem like I can modify the User struct in each Video inside videoList once the JSON decoder initializes the list of videos, due to the fact that VideoViewModel is a MainActor. I would prefer to have everything downloaded in one go, so that when the user enters a subview, there is no loading time.
You are correct that you can't modify the structs once they are initialized, because all of their properties are let variables; however, you can modify the videoList in VideoViewModel, allowing you to dispense with the Dictionary.
#MainActor
class VideoViewModel: ObservableObject {
#Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()
let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = try await Self.getSocialIDForAll(in: responseResult)
}
private static func updatedWithSocialID(_ user: User) async throws -> User {
return User(id: user.id, name: user.name, socialID: try await user.getSocialID())
}
private static func updatedWithSocialID(_ video: Video) async throws -> Video {
return Video(id: video.id, title: video.title, uploadUser: try await updatedWithSocialID(video.uploadUser))
}
static func getSocialIDForAll(in videoList: [Video]) async throws -> [Video] {
return try await withThrowingTaskGroup(of: Video.self) { group in
videoList.forEach { video in
group.addTask {
return try await self.updatedWithSocialID(video)
}
}
var newVideos: [Video] = []
newVideos.reserveCapacity(videoList.count)
for try await video in group {
newVideos.append(video)
}
return newVideos
}
}
}
Using a view model object is not standard for SwiftUI, it's more of a UIKit design pattern but actually using built-in child view controllers was better. SwiftUI is designed around using value types to prevent the consistency errors typical for objects so if you use objects then you will still get those problems. The View struct is designed to be the primary encapsulation mechanism so you'll have more success using the View struct and its property wrappers.
So to solve your use case, you can use the #State property wrapper, which gives the View struct (which has value semantics) reference type semantics like an object would, use this to hold the data that has a lifetime matching the View on screen. For the download, you can use async/await via the task(id:) modifier. This will run the task when the view appears and cancel and restart it when the id param changes. Using these 2 features together you can do:
#State var socialID
.task(id: videoID) { newVideoID in
socialID = await Social.getSocialID(videoID: newViewID)
}
The parent View should have a task that got the video infos.

How to use SwiftUI Picker to update a Realm to-one relationship?

I have the following Realm schema where a Race is done on a Track:
final class Race: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var track: Track?
#Persisted var duration: Int = 45
}
final class Track: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String = "Imola"
#Persisted var country: String = "🇮🇹"
#Persisted(originProperty: "tracks") var group: LinkingObjects<TrackGroup>
}
final class TrackGroup: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var tracks = RealmSwift.List<Track>()
}
In my ContentView I have an Add Button that opens a sheet (AddRaceView). The new Race is already created when the sheet appears. Now, I want to use a Picker for the Track selection for our newly created Race.
The following code does not update the Track for the editable Race, and I do not understand why:
struct AddRaceView: View {
#ObservedRealmObject var race: Race
#ObservedRealmObject var trackGroup: TrackGroup
var body: some View {
Form {
chooseTrackSection
raceDurationSection
}
}
#State private var trackPickerVisible = false
var chooseTrackSection: some View {
Section(header: Text("Track")) {
Button {
withAnimation(.easeIn) {
self.trackPickerVisible.toggle()
}
} label: {
HStack {
Text(race.track?.name ?? "")
Spacer()
Image(systemName: "arrow.turn.right.down")
}
}
if trackPickerVisible {
// HERE: Selection is not processed.
Picker(selection: $race.track, label: Text("Track")) {
ForEach(trackGroup.tracks) {
Text($0.name)
}
}
.pickerStyle(.wheel)
}
}
}
Updating other values in Race (like duration) does work! When Track is a String for example, I can use the Picker to make a selection. The problem must be connected to the fact that I'm trying to change a Realm object/relationship.
There are three things that need to be taken into account:
Picker needs to know where to find the values. The value can be specified manually by adding .tag(value) to the elements. While ForEach provides implicit tags for objects that conform to Identifiable, the type doesn't match in your case (needs to be Optional<Track> instead of Track).
The Picker compares all tag values to the selection to find out which item is currently selected. The comparison fails if the objects are not from the same Realm instance. Unfortunately, there isn't currently any way to specify a Realm for ObservedResults or an ObservedRealmObject.
Referencing objects from a frozen Realm doesn't work, so they (or their Realm) have to be thawed first.
Code:
// Fetch the Tracks from the Race's Realm by finding the TrackGroup by primaryKey
if let tracks = race.realm?.thaw().object(ofType: TrackGroup.self, forPrimaryKey: trackGroup._id)?.tracks {
Picker("Track", selection: $race.track) {
ForEach(tracks) { track in
Text(track.name)
.tag(Optional(track)) // Specify the value, making it optional to exactly match the expected type
}
}
}

SwiftUI List with sections using dictionary of array

I have an array of users returned. I want to group them by created date and then display them in SwiftUI List with sections. The section title is the date. After I group the users I end up with a Dictionary of user arrays and the key is the date that group all users that have been created in the same date I want to use the key as a section and the value (user array) as List rows. It seems that List only works with array. Any clean solution to prepare the data for the List so it can display the sections and its content ?
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var interactor: Interactor
var body: some View {
List(interactor.users) { user in // Error: Cannot convert value of type '[String : [User]]' to expected argument type 'Range<Int>'
}
.onAppear {
interactor.loadUsers()
}
}
}
class Interactor: ObservableObject {
#Published private(set) var users = [String: [User]]()
func loadUsers() {
let allUsers = [User(id: "1", name: "Joe", createdAt: Date()),
User(id: "2", name: "Jak", createdAt: Date())]
users = Dictionary(grouping: allUsers) { user in
let dateFormatted = DateFormatter()
dateFormatted.dateStyle = .short
return dateFormatted.string(from: user.createdAt)
}
}
}
struct User: Identifiable {
let id: String
let name: String
let createdAt: Date
}
You can use sections inside List, and make use of its header parameter to pass Dates. I have already fetched all dates as [String] by using separate function. One thing you need to figure is the way you are going to pass multiple keys to fetch data for different date keys that you will have. May be you can create that first by using [Users] objects and save all keys.
Below is the solution to your issue-:
ContentView-:
//// Created by TUSHAR SHARMA on 06/02/21.
////
//
import SwiftUI
struct ContentView: View {
#EnvironmentObject var interactor: Interactor
var body: some View {
List {
ForEach(getAllDates(),id:\.self) { dates in
Section(header: Text(dates)) {
ForEach(interactor.users[dates] ?? []) { users in
Text(users.name)
}
}
}
}
}
func getAllDates() -> [String]{
let getObjects = interactor.users[Date().stringFromDate()] ?? []
var dateArray : [String] = []
for getData in getObjects{
dateArray.append(getData.createdAt.stringFromDate())
}
let unique = Array(Set(dateArray))
return unique
}
}
extension Date{
func stringFromDate() -> String{
let dateFormatted = DateFormatter()
dateFormatted.dateStyle = .short
return dateFormatted.string(from: self)
}
}
class Interactor: ObservableObject {
#Published private(set) var users = [String: [User]]()
init() {
loadUsers()
}
func loadUsers() {
let allUsers = [User(id: "1", name: "Joe", createdAt: Date()),
User(id: "2", name: "Jak", createdAt: Date())]
users = Dictionary(grouping: allUsers) { user in
user.createdAt.stringFromDate()
}
}
}
struct User: Identifiable {
let id: String
let name: String
let createdAt: Date
}
#main view
// Created by TUSHAR SHARMA on 07/01/21.
//
import SwiftUI
#main
struct WaveViewApp: App {
let interactor = Interactor()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(interactor)
}
}
}

Swift ForEach Issues with JSON Dictionary

I am trying to build an application which receives a JSON Object from an API endpoint, which then I want to list out in the view. I've watched a lot of videos on this topic, but in each video they use very simplistic JSON Objects as examples and therefore the code they write doesn't really seem to transfer over, giving me errors no matter how I try to format it. The code is as follows
import SwiftUI
import Combine
import Foundation
public struct ActivityModel: Codable, Identifiable {
public let id: Int
public let name: String
public let activity_desc: String?
}
public struct ActivitiesModel2: Codable {
public let location: String
public let popular: [String:ActivityModel]
}
public struct ActivitiesModel: Codable {
public let activities: ActivitiesModel2
}
public class ActivityFetcher: ObservableObject {
var activities: ActivitiesModel?
init(){
guard let url = URL(string: "https://mywebsite.com/api/loadapi") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode(ActivitiesModel.self, from: d)
DispatchQueue.main.async {
self.activities = decodedLists
}
} else {
print("No Data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct ActivityGuestView: View {
let networkingServiceGeneral = NetworkingServiceGeneral()
#ObservedObject var viewRouter: ViewRouter
#ObservedObject var fetcher = ActivityFetcher()
var body: some View {
// This is where my issues start
List(fetcher.activities?.activities.popular) { result in
VStack {
Text(result.name)
Text(result.activity_desc)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
This code, as I put it, gives me 5 errors. They are the following;
- Initializer 'init(_:rowContent:)' requires that '(key: String, value: ActivityModel)' conform to Identifiable
- Initializer 'init(_:rowContent:)' requires that '[String : ActivityModel]' conform to 'RandomAccessCollection'
-Value of optional type '[String : ActivityModel]?' must be unwrapped to a value of type '[String : ActivityModel]'
- Coalesce using '??' to provide a default when the optional value contains 'nil'
- Force-unwrap using '!' to abort execution if the optional value contains 'nil'
Some of these errors have options to fix it, but when I press fix it adds code but doesn't actually fix the error so I figured to just include them anyways. I'm still fairly new to Swift, but I know what some of it is asking, particularly the conforming to Identifiable, but it says that struct ActivitiesModel does not conform to identifiable when I try to add the tag, and the JSON Object doesn't have an ID for that section, so I can't ask the ID to make it identifiable.
Any help would be greatly appreciated, this has kind of been a wall right now.
Here's the JSON
"activities": {
"location": "Dallas",
"popular": {
"10": {
"id": 38,
"name": "Adventure Landing Dallas",
"activity_desc": "Aquatic complex chain with additional land attractions including mini-golf, laser tag & go-karts.",
},
"12": {
"id": 40,
"name": "Jumpstreet",
"activity_desc": "None provided.",
},
}
}
}
You have a couple of issues here.
First, dictionaries and Lists aren't compatible; You really need an array. As the errors say, the item that is supplied to a List needs to confirm to Identifiable and RandomAccessCollection. A dictionary conforms to neither and you can't really make it do so.
Your second issue is that fetcher.activities is an optional and the List initialiser can't accept an optional. The compiler is suggesting a couple of alternatives - Supply a default value using the nil coalescing operator (??) or force unwrap using ! (Which will crash, since you know that fetcher.activities is going to be nil initially.
The root cause of the first problem is that your JSON has variable keys rather than a simple array of activities. This is not a great idea and you should really put some work in on the server side to eliminate the meaningless numeric key and have a simple array, as per my comment, however you have indicated that you can't do this.
Given this, another approach is to expose the dictionary values as an array using a computed property:
public struct ActivitiesModel2: Codable {
public let location: String
private var popular: [String:ActivityModel]
public var popularActivities: [ActivityModel] {
get {
return Array(self.popular.values)
}
}
}
While we are here, we can fix that un-Swifty activity_desc with a CodingKeys enumeration:
public struct ActivityModel: Codable, Identifiable {
public let id: Int
public let name: String
public let activityDesc: String?
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case activityDesc = "activity_desc"
}
}
Now, you can re-write your view to use the new computed property and to handle the optional:
struct ActivityGuestView: View {
let networkingServiceGeneral = NetworkingServiceGeneral()
#ObservedObject var viewRouter: ViewRouter
#ObservedObject var fetcher = ActivityFetcher()
var body: some View {
// This is where my issues start
List(fetcher.activities?.activities.popularActivities ?? []) { result in
VStack {
Text(result.name)
Text(result.activity_desc)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}

Resources