How to Deep Link Widget with data pulled from Core Data? - ios

I'm trying to get deep linking working for my app's widgets. The widgets simply display an image from a Core Data entity: Project. I'd like for when a user taps on the widget, it opens up the ProjectDetailView() that the image is a member of.
Everything I read says I should be able to add Link(destination: url) but I'm not getting anywhere with that. In my app, project is pulled from Core Data, but it has no url attribute. I feel like it doesn't need one based on the examples I've seen?
The below code gives me the error Value of type 'FetchedResults<Project>' has no member 'url' which is true.. it doesn't. I don't understand how to get the url to the ProjectDetailView() the image is included in. Thanks so much for taking a look!
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct WidgetEntryView : View {
var entry: Provider.Entry
#State var projectImage1: Data = .init(count: 0)
#FetchRequest(entity: Project.entity(), sortDescriptors: []) var project: FetchedResults<Project>
var body: some View {
if let randomProject: Project = project.randomElement() {
Link (destination: project.url) {
Image(uiImage: UIImage(data: randomProject.image1 ?? self.projectImage1) ?? UIImage(imageLiteralResourceName: "icon"))
.resizable()
.aspectRatio(contentMode: .fill)
}
}
}
}

If you look at the url in their CharacterDetail.swift it is something like "game:///spouty" game to represent the app the :/// to conform to url and something unique.
So all you need to get a url for for your file is to follow the same pattern.
yourApp:///anyUniqueVariable
You can achieve this in an extension of your object class with a computed variable.
extension Project{
var url: URL{
return URL(string: "yourApp:///\(self.anyUniqueVariable)")
}
}
You keep saying you have a CoreData object that isn't really relevant because the actual connection is made in with SwiftUI and how you "trigger" your NavigationLink to the View that displays your object.
If you look at the ContentView.swift you'll see
.onOpenURL(perform: { (url) in
self.pandaActive = url == CharacterDetail.panda.url
self.spoutyActive = url == CharacterDetail.spouty.url
self.eggheadActive = url == CharacterDetail.egghead.url
})
They are using the NavigationLink init with isActive and triggering the making it true when the "received url == the characters url".
You could use that method (might not be the best since you likely have more than 3 objects) or the init with NavigationLink that has selection and set the tag/selection variable to the url you received in openURL.
It just depends on how you want to trigger your NavigationLink.

Related

Basic understand issues working with KeychainAccess framework

strong textI'm new to Swift and been trying to develope a password manager as a starter project (not sure if that's a bit too much, but hey here we are). ^^
I wanted to use Apples Keychain to securely store password (to be exact: email, password, comment, website, and an Array containing entries the user can modify with a title and a text element). To make life a bit easier, I'm using this wrapper: https://github.com/kishikawakatsumi/KeychainAccess
The thing I don't understand is how to iterate over each of the items currently stored in my Keychain. There's a test project in the repo, that didn't helped me at all.
Here's my attempt to iterate over all Keychain entries with a ForEach loop:
import SwiftUI
import KeychainAccess
struct PasswordsView: View {
// Makes the passwordEntry available as an object
#EnvironmentObject var entryData: EntryData
// Used to create a new password entry
#State private var isAddingNewEntry = false
#State private var newEntry = PasswordEntry()
// NEW get the keychain entries
let passwordItems = Keychain().allItems()
var body: some View {
NavigationView {
// List with keychain entries
List {
Section(
header: Text("entries")
.font(.callout)
.foregroundColor(.secondary)
.fontWeight(.bold)
) {
ForEach(passwordItems) { passwordItems in
HStack {
Text((passwordItems["key"] as? String)! ?? <#default value#>)
Text((passwordItems["value"] as? String)! ?? <#default value#>)
}
}
}
}
// Navigationbar with title & plus icon to add new entries
.navigationTitle("Passwords")
.toolbar{
ToolbarItem {
// Add the plus icon to the toolbar
Button {
newEntry = PasswordEntry()
// Set isAddingNewEntry to true do display the sheet
isAddingNewEntry = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isAddingNewEntry) {
NavigationView {
EntryEditor(passwordEntry: newEntry, isNew: true)
}
}
}
}
}
What is wrong and/or missing to make this work? I'm getting a "Referencing initializer 'init(_:content:)' on 'ForEach' requires that '[String : Any]' conform to 'Identifiable'" error on the ForEach loop at the moment.
And what I like to know: What exactly is the Keychain returning when accessing all items? I thought it was a dict, but maybe I'm wrong?
P.S.: Feel free to let me know if the keychain is the absolute wrong place to store credentials AND "metadata"... I thought about SQLite, but not beeing able to encrypt things seemed like a major disadvantange for my purpose...

Object's property not updating in array SwiftUI

I'm picking the photos from the PHPicker and displaying them in the VStack using the ForEach loop in SwiftUI. I'm wrapping the image in an Object with 3 properties.
class UploadedImage: Hashable, Equatable {
var id: Int
var image : UIImage
#State var quantity: Int
init(id: Int, image: UIImage, quantity: Int) {
self.id = id
self.image = image
self.quantity = quantity
}
static func == (lhs: UploadedImage, rhs: UploadedImage) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
}
Then I'm adding that object in the array after the picker select the images, and then displaying on the screen using the ForEach.
if !uploadedImages.isEmpty {
ScrollView {
VStack{
ForEach(uploadedImages, id: \.self) { selectedImage in
HStack{
ZStack{
// some other code to display image...
}
Spacer()
HStack{
Button(action: {
if let index = self.uploadedImages.firstIndex(where: {$0.id == selectedImage.id}){
uploadedImages[index] = UploadedImage(id: selectedImage.id, url: selectedImage.url, quantity: (1))
}
}, label: {
Text("-")
})
Text("\(selectedImage.quantity)")
Button(action: {
if let index = self.uploadedImages.firstIndex(where: {$0.id == selectedImage.id}){
uploadedImages[index] = UploadedImage(id: selectedImage.id, url: selectedImage.url, quantity: selectedImage.quantity + 1)
}
}, label: {
Text("+")
})
}
}
.padding()
.border(Color.black, width: 2)
.cornerRadius(6)
}
}
}
}
}
In the ForEach loop, I'm showing two buttons, +, - to increase and decrease the quantity.
On that button the action is the following code.
if let index = self.uploadedImages.firstIndex(where: {$0.id == selectedImage.id}){
uploadedImages[index] = UploadedImage(id: selectedImage.id, url: selectedImage.url, quantity: selectedImage.quantity + 1)
}
But the quantity is not updating. I'm not sure why.
When I change the id in the new updated Object.
uploadedImages[index] = UploadedImage(id: 89282, url: selectedImage.url, quantity: selectedImage.quantity + 1)
Then the quantity is updating only once.
Any good solution to increase and decrease the quantity?
Data flow in SwiftUI can be a bit tricky to get used to initially, depending on what framework you’re coming from.
There are various ways to approach it from high-level perspectives, but given that your question is about this localized issue of a single View, its model, and a button, I will focus on data from a single View’s perspective.
To then understand how data flows between Views, how it interacts with SwiftUI’s View lifecycle, and more, I would recommend these two WWDC sessions:
Data Essentials in SwiftUI
Demystify SwiftUI
There’s also this document from Apple with a decent breakdown:
Model data.
And this section in the SwiftUI Tutorials: Managing state and life cycle
Returning to the View’s perspective.
The way you set up your data inside a View will be quite different based on two characteristics of your data:
Is the data a value type or a reference type?
Do you want SwiftUI to handle the lifecycle of your data or do you want to manage it yourself?
So on the simple end of that matrix, you have a value type that you let SwiftUI handle for you.
This is what #State is for.
Inside a View, you might use #State like this to deal with a string:
#State var name = “Bob”
but a struct would work as well:
#State var selectedUser = MyUserStruct()
The important thing to know about #State is that it is explicitly meant for this simple end of the possible scenarios.
#State
is intended to be used only within Views
needs the data to be a value type to function properly
is bound to the lifecycle of the View
It’s meant for data that represents state internal to your Views.
Note, that nothing has to be done to our data in this case, to make SwiftUI understand when to update the View. (At least for most use cases.)
Moving up in complexity:
What if you want SwiftUI to handle the data, but you are dealing with a reference type?
This is the setup you currently have.
In contrast to the value type example, SwiftUI needs additional help understanding when to update the Views that depend on reference types.
Therefore, any classes that should drive View updates need to conform to the ObservableObject protocol.
Further, within those classes, only properties marked #Published will actually drive View updates when those properties change.
To then use an #ObservableObject inside a View, you have a choice of a couple of property wrappers with very different behaviors:
#StateObject, #ObservedObject, and #EnvironmentObject.
#StateObject is the closest in its behavior to #State. The object is managed by SwiftUI and the lifecycle of an ObservableObject that is initialized as a #StateObject is bound to the lifecycle of the View in which it is initialized.
#ObservedObject and #EnvironmentObject don’t match our current location in the matrix of complexity, as they are meant for the case in which you don’t want SwiftUI to handle your data’s lifecycle (or not as directly).
But this actually gives us sufficient information to restructure your example and stay within our single View perspective.
Let’s first take your example literally and pretend that we just want to make those localized updates in an array, and that’s its.
For this case, #State would actually work, but we will need to change your UploadedImage model to a struct:
struct UploadedImage: Identifiable {
let id: Int
let image: Image
var quantity: Int
}
Two notes:
I changed your image to use SwiftUI’s Image type instead of UIKit’s UIImage. The latter would still work for the purpose of this example if you absolutely need to deal with UIImage instances.
Identifiable is a protocol that tells SwiftUI to use the id property (which you already had anyways) to identify data in places like ForEach. Therefore, we don’t have to conform to Hashable, nor do we need to tell the ForEach explicitly how to identify each UploadedImage.
Now we’ll use this in the View, via #State:
#State var uploadedImages = [
UploadedImage(id: 1, image: someImage, quantity: 4),
UploadedImage(id: 2, image: someImage, quantity: 8),
UploadedImage(id: 3, image: someImage, quantity: 16)
]
And as mentioned above, the ForEach can be simplified to:
ForEach(uploadedImages) { selectedImage in
// ...
}
And that’s actually all that is needed to make your example work.
But as others have pointed out, it’s likely that you may have other logic to handle, and it would potentially be nice to split out some of this non-UI logic to a separate view model, have the View interact with that model, and observe it for changes.
Usually, view models take the form of a class, so we change our approach to the reference type route we discussed above: our view model will need to conform to ObservableObject, use #Published to surface any data that should drive updates, and it gets handed to SwiftUI to manage via #StateObject.
Combined, this solution looks like this:
#MainActor class UploadedImagesViewModel: ObservableObject {
#Published var images = [
UploadedImage(id: 3, image: someImage, quantity: 4),
UploadedImage(id: 4, image: someImage, quantity: 8),
UploadedImage(id: 5, image: someImage, quantity: 16)
]
func increment(_ selectedImage: UploadedImage) {
if let index = self.images.firstIndex(where: {$0.id == selectedImage.id}){
images[index].quantity += 1
}
}
func decrement(_ selectedImage: UploadedImage) {
if let index = self.images.firstIndex(where: {$0.id == selectedImage.id}){
images[index].quantity -= 1
}
}
}
struct UploadedImage: Identifiable {
let id: Int
let image: Image
var quantity: Int
}
struct UploadedImagesView: View {
#StateObject var viewModel = UploadedImagesViewModel()
var body: some View {
if !viewModel.images.isEmpty {
ScrollView {
VStack{
ForEach(viewModel.images) { selectedImage in
HStack{
ZStack{
// some other code to display image...
}
Spacer()
HStack{
Button(action: {
viewModel.decrement(selectedImage)
}, label: {
Text("-")
})
Text("\(selectedImage.quantity)")
Button(action: {
viewModel.increment(selectedImage)
}, label: {
Text("+")
})
}
}
.padding()
.border(Color.black, width: 2)
.cornerRadius(6)
}
}
}
}
}
}
A few final final notes:
As you can see, the actual data remained in struct form. There are cases where you may actually want an array of other reference types, e.g. of other view models, but those cases are somewhat rare and hard to make work properly. This combination of a class for your ViewModel and a struct for the actual data model is the most common approach.
#MainActor is something related to Swift’s new concurrency model, and the details are out of the scope of this answer. But it’s a good idea to apply to view models / ObservableObjects in general, as it will ensure (and via compiler errors guide you to) only update the ViewModel from the main thread.

What does 'identity' mean in SwifUI and how do we change the 'identity' of something

I'm new to SwiftUI and I'm having problems with presenting Alerts back-to-back.
The description of the .alert(item:content:) modifier has this written in it's definition:
/// Presents an alert.
///
/// - Parameters:
/// - item: A `Binding` to an optional source of truth for the `Alert`.
/// When representing a non-nil item, the system uses `content` to
/// create an alert representation of the item.
///
/// If the identity changes, the system will dismiss a
/// currently-presented alert and replace it by a new alert.
///
/// - content: A closure returning the `Alert` to present.
public func alert<Item>(item: Binding<Item?>, content: (Item) -> Alert) -> some View where Item : Identifiable
I'm particularly interested in the If the identity changes, the system will dismiss a currently-presented alert and replace it by a new alert part. Since I want Alerts to be presented back-to-back, if I'm somehow able to change the 'identity', I'll be able to achieve the functionality that I want - which is having the system dismiss the currently-presented alert and replacing the old Alert with a new Alert (back-to-back).
If someone can explain to me what 'identity' is and how I can change the 'identity' of something I'll be extremely grateful.
(Or if you know a better way to present alerts back-to-back that'll also be very very helpful.)
Thanks in advance!
Find below demo of alert-by-item usage. And some investigation results about changing identity as documented.
As you find experimenting with example (by tap on row) alert activated by user interaction works fine, but changing identity programmatically, as documented, seems not stable yet, however alert is really updated.
Tested with Xcode 11.4 / iOS 13.4
struct SomeItem: Identifiable { // identifiable item
var id: Int // identity
}
struct DemoAlertOnItem: View {
#State private var selectedItem: SomeItem? = nil
var body: some View {
VStack {
ScrollView (.vertical, showsIndicators: false) {
ForEach (0..<5) { i in
Text("Item \(i)").padding()
.onTapGesture {
self.selectedItem = SomeItem(id: i)
// below simulate change identity while alert is shown
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.selectedItem = nil // works !!
// self.selectedItem?.id = 100 // crash !!
// self.selectedItem = SomeItem(id: 100) // crash !!
}
}
}
}
}
.alert(item: self.$selectedItem) { item in
Alert(title: Text("Alert"), message: Text("For item \(item.id)"), dismissButton: .default(Text("OK")))
}
}
}
Notice how this method requires a Binding<Item?>, and that Item should be Identifiable. For the Binding<Item?> parameter, you're supposed to pass in a "source of truth" that controls what the alert shown looks like, or whether the alert shows at all. When this source of truth changes (i.e. becomes something else), the view will update the alert.
But here's the problem, how does SwiftUI know what does "change" mean in the context of your model? Let's say Item is a Person class that you wrote. Person has a name and age. It is your job to tell SwiftUI, that "a Person becomes a totally different person when its name changes". (Of course, you could have some other definition of what is meant by "a person changes" here. This definition is just an example.)
struct Person : Identifiable {
var id: String {
name
}
let name: String
let age: Int
}
This is why Item must be Identifiable. Item.id is thus the "identity".
Note that Identifiable is different from Equatable, in that Identifiable asks the question "what makes this person a different person?" whereas Equatable asks "what result would you want == to give?". See this for another example.
how do we change the 'identity' of something?
Just change the binding you pass in (e.g. setting the #State that the binding is based on) in such a way that its id changes.

SwiftUI load very long list in async way

I've a navigation and detail view that is sending a dictionary of date (key) and array of an struct (but it's not important the struct, it contains array of string and other stuff.
If I send a very very long dictionary, the app is freezing in the selected row and the detail appears once the List finished to load each record.
struct DetailView: View {
var selectedChat: [Date: [TextStruct]]? // you can try with [Date: [String]]?
var body: some View {
List
{
ForEach(self.selectedChat.keys.sorted(), id: \.self)
{ key in //section data
Section(header:
Text("\(self.selectedChat[key]![0].date)
{
ForEach(self.selectedChat[key]!, id:\.self) {sText in
// my ChatView(sText) ....
}
}
}
}
I've tried to load some rows at the start by adding this var
#State private var dateAndText: [Date: [TextStruct]] = [:]
substitute the code above (self.selectedChat) whit self.dateAndText and on .onAppear:
.onAppear {
if let chat = self.selectedChat {
let keysDateSorted = chat.allText.keys.sorted()
self.chatLeader = chat.chatLeader
for key in keysDateSorted.prefix(30) {
self.dateAndText[key] = chat.allText[key]
}
DispatchQueue.global(qos: .utility).async {
self.dateAndText = chat.allText
self.progressBarValue = 1
}
}
}
With this solution, once I push the row, immediately I can see the first 30 records, and it is ok, but I can't scroll until all the records are loaded. I know there is a way to load the array only if the user is scrolling at the end of the list, but I want to load all the list also if the user don't scroll at the end.
So, there is a way to load the list partially (like send and update the array each 100 records) and in async way (in order to don't freeze the display for bad user experience)?
You are almost certainly running into the issues described and fixed here by Paul Hudson.
SwiftUI is trying to animate all of the changes so if you use his hack around the issue it should work but you will lost all animations between changes of the list.
Apple devs responded to him and Dave DeLong who were discussing it on Twitter, they said that it is definitely an issue on their end that they hope to fix.
tldr of the article:
Add .id(UUID()) to the end of your List's initializer.

SwiftUI custom View's ViewBuilder doesn't re-render/update on subclassed ObservedObject update

This one I've been researching for a few days, scouring the Swift & SwiftUI docs, SO, forums, etc. and can't seem to find an answer.
Here is the problem;
I have a SwiftUI custom View that does some state determination on a custom API request class to a remote resource. The View handles showing loading states and failure states, along with its body contents being passed through via ViewBuilder so that if the state from the API is successful and the resource data is loaded, it will show the contents of the page.
The issue is, the ViewBuilder contents does not re-render when the subclassed ObservedObject updates. The Object updates in reaction to the UI (when buttons are pressed, etc.) but the UI never re-renders/updates to reflect the change within the subclassed ObservedObject, for example the ForEach behind an array within the subclassed ObservedObject does not refresh when the array contents change. If I move it out of the custom View, the ForEach works as intended.
I can confirm the code compiles and runs. Observers and debugPrint()'s throughout show that the ApiObject is updating state correctly and the View reflects the ApiState change absolutely fine. It's just the Content of the ViewBuilder. In which I assume is because the ViewBuilder will only ever be called once.
EDIT: The above paragraph should have been the hint, the ApiState updates correctly, but after putting extensive logging into the application, the UI was not listening to the publishing of the subclassed ObservedObject. The properties were changing and the state was too, but the UI wasn't being reactive to it.
Also, the next sentence turned out to be false, I tested again in a VStack and the component still didn't re-render, meaning I was looking in the wrong place!
If this is the case, how does VStack and other such elements get around this?
Or is it because my ApiObjectView is being re-rendered on the state change, in which causes the child view to 'reset'? Although in this circumstance I'd expect it to then take on the new data and work as expected anyway, its just never re-rendering.
The problematic code is in the CustomDataList.swift and ApiObjectView.swift below. I've left comments to point in the right direction.
Here is the example code;
// ApiState.swift
// Stores the API state for where the request and data parse is currently at.
// This drives the ApiObjectView state UI.
import Foundation
enum ApiState: String
{
case isIdle
case isFetchingData
case hasFailedToFetchData
case isLoadingData
case hasFailedToLoadData
case hasUsableData
}
// ApiObject.swift
// A base class that the Controllers for the app extend from.
// These classes can make data requests to the remote resource API over the
// network to feed their internal data stores.
class ApiObject: ObservableObject
{
#Published var apiState: ApiState = .isIdle
let networkRequest: NetworkRequest = NetworkRequest(baseUrl: "https://api.example.com/api")
public func apiGetJson<T: Codable>(to: String, decodeAs: T.Type, onDecode: #escaping (_ unwrappedJson: T) -> Void) -> Void
{
self.apiState = .isFetchingData
self.networkRequest.send(
to: to,
onComplete: {
self.apiState = .isLoadingData
let json = self.networkRequest.decodeJsonFromResponse(decodeAs: decodeAs)
guard let unwrappedJson = json else {
self.apiState = .hasFailedToLoadData
return
}
onDecode(unwrappedJson)
self.apiState = .hasUsableData
},
onFail: {
self.apiState = .hasFailedToFetchData
}
)
}
}
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
// Subclassed from the ApiObject, inheriting ObservableObject
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = []
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
This is the View that has the problem with re-rendering its ForEach on the ObservedObject change to its bound array property.
// CustomDataList.swift
// This is the SwiftUI View that drives the content to the user as a list
// that displays the CustomDataController.customData.
// The ForEach in this View
import SwiftUI
struct CustomDataList: View
{
#ObservedObject var customDataController: CustomDataController = CustomDataController()
var body: some View
{
ApiObjectView(
apiObject: self.customDataController,
onQuit: {}
) {
List
{
Section(header: Text("Custom Data").padding(.top, 40))
{
ForEach(self.customDataController.customData, id: \.self, content: { customData in
// This is the example that doesn't re-render when the
// customDataController updates its data. I have
// verified via printing at watching properties
// that the object is updating and pushing the
// change.
// The ObservableObject updates the array, but this ForEach
// is not run again when the data is changed.
// In the production code, there are buttons in here that
// change the array data held within customDataController.customData.
// When tapped, they update the array and the ForEach, when placed
// in the body directly does reflect the change when
// customDataController.customData updates.
// However, when inside the ApiObjectView, as by this example,
// it does not.
Text(customData.textProperty)
})
}
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Learn"))
.onAppear() {
self.customDataController.fetch()
}
}
}
struct CustomDataList_Previews: PreviewProvider
{
static var previews: some View
{
CustomDataList()
}
}
This is the custom View in question that doesn't re-render its Content.
// ApiObjectView
// This is the containing View that is designed to assist in the UI rendering of ApiObjects
// by handling the state automatically and only showing the ViewBuilder contents when
// the state is such that the data is loaded and ready, in a non errornous, ready state.
// The ViewBuilder contents loads fine when the view is rendered or the state changes,
// but the Content is never re-rendered if it changes.
// The state renders fine and is reactive to the object, the apiObjectContent
// however, is not.
import SwiftUI
struct ApiObjectView<Content: View>: View {
#ObservedObject var apiObject: ApiObject
let onQuit: () -> Void
let apiObjectContent: () -> Content
#inlinable public init(apiObject: ApiObject, onQuit: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self.apiObject = apiObject
self.onQuit = onQuit
self.apiObjectContent = content
}
func determineViewBody() -> AnyView
{
switch (self.apiObject.apiState) {
case .isIdle:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isFetchingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isLoadingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .hasFailedToFetchData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasFailedToLoadData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasUsableData:
return AnyView(
VStack
{
self.apiObjectContent()
}
)
}
}
var body: some View
{
self.determineViewBody()
}
}
struct ApiObjectView_Previews: PreviewProvider {
static var previews: some View {
ApiObjectView(
apiObject: ApiObject(),
onQuit: {
print("I quit.")
}
) {
EmptyView()
}
}
}
Now, all the above code works absolutely fine, if the ApiObjectView isn't used and the contents placed in the View directly.
But, that is horrendous for code reuse and architecture, this way its nice and neat, but doesn't work.
Is there any other way to approach this, e.g. via a ViewModifier or a View extension?
Any help on this would be really appreciated.
As I said, I can't seem to find anyone with this problem or any resource online that can point me in the right direction to solve this problem, or what might be causing it, such as outlined in documentation for ViewBuilder.
EDIT: To throw something interesting in, I've since added a countdown timer to CustomDataList, which updates a label every 1 second. IF the text is updated by that timer object, the view is re-rendered, but ONLY when the text on the label displaying the countdown time is updated.
Figured it out after pulling my hair out for a week, its an undocumented issue with subclassing an ObservableObject, as seen in this SO answer.
This is particularily annoying as Xcode obviously prompts you to remove the class as the parent class provides that inheritence to ObservableObject, so in my mind all was well.
The fix is, within the subclassed class to manually fire the generic state change self.objectWillChange.send() via the willSet listener on the #Published variable in question, or any you require.
In the examples I provided, the base class ApiObject in the question remains the same.
Although, the CustomDataController needs to be modified as follows:
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As soon as I added that manual publishing, the issue is resolved.
An important note from the linked answer: Do not redeclare objectWillChange on the subclass, as that will again cause the state not to update properly. E.g. declaring the default
let objectWillChange = PassthroughSubject<Void, Never>()
on the subclass will break the state updating again, this needs to remain on the parent class that extends from ObservableObject directly, either my manual or automatic default definition (typed out, or not and left as inherited declaration).
Although you can still define as many custom PassthroughSubject declarations as you require without issue on the subclass, e.g.
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
var customDataWillUpdate = PassthroughSubject<[CustomDataStruct], Never>()
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// Custom state change handler.
self.customDataWillUpdate.send(newValue)
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As long as
The self.objectWillChange.send() remains on the #Published properties you need on the subclass
The default PassthroughSubject declaration is not re-declared on the subclass
It will work and propagate the state change correctly.

Resources