I have two classes, one is the ContentView who is displaying the information from my data source. The data source is my CharacterRepository.
What I'm struggling with right now is making sure I always have a sorted list inside of my CharacterRepository.
Here's the code I have so far:
class CharacterRepository: ObservableObject {
#Published public var characters = [Character(name: "Nott the Brave",
initiative: 23,
isActive: false),
Character(name: "Caduceus Clay",
initiative: 2,
isActive: false),
...]
...
}
and
struct InitiativeTrackerScreen: View {
#EnvironmentObject var characterRepository: CharacterRepository
var body: some View {
NavigationView {
VStack {
List {
ForEach(characterRepository.characters) { entry in
CharacterListElement(character: entry)
}
...
Now my intended approach would be something like turning the characters variable into a computed property that runs the "sorted by" function every time the get gets executed. Unfortunately, Binding to a computed property is not yet possible in SwiftUI (not sure, will it ever be?).
Can someone please help me with this? I don't want to go back to the old approach of sorting and redrawing every time something changes. That's not why I am using SwiftUI with ist sweet bindings.
I think it may be unnecessary overhead to be sorting the data in the property getter.
The simple (and performance-conscious) solution is to sort the data when it changes, and then to update a #Published non-computed property and bind to that:
class CharacterRepository: ObservableObject {
func updateCharacters(_ characters: [Character]) {
self.characters = characters.sorted() // or whatever sorting strategy you need...
}
#Published public var characters = [Character(name: "Nott the Brave",
initiative: 23,
isActive: false),
Character(name: "Caduceus Clay",
initiative: 2,
isActive: false),
...]
...
}
If you prefer to be über-purist about this at the cost of performance, then you can manually create a Binding - something like this:
class CharacterRepository: ObservableObject {
let sortedCharacters: Binding<[Character]>
...
init() {
self.sortedCharacters = .init(get: { return characters.sorted() }, set: { self.characters = $0 })
}
...
}
I like to answer by related question. For what do you ever need some binding to computed property?
class CharacterRepository: ObservableObject {
#Published public var characters = [Character(name: "Nott the Brave",
initiative: 23,
isActive: false),
Character(name: "Caduceus Clay",
initiative: 2,
isActive: false),
...]
...
var sorted: [Character] {
characters.sorted()
}
}
is all you need.
Related
I'm struggling to find how to bind an array value to a Toggle view in SwiftUI.
Lets says I have an observabled class with a Boolean array:
class TestClass: ObservabledObject {
#Published var onStates: [Bool] = [true, false, true]
static let shared = TestClass()
}
and in a View I have
...
Toggle(isOn: TestClass.shared.$onStates[0]) { // Throws error 'Referenceing subscript 'subscript(_:)' requires wrapped value of type '[Bool]'
Text("Example Toggle")
}
Why is it seemingly impossible to bind a particular array value to the toggle button?
Thanks.
We need observer in view for observable object, so fixed variant is
#StateObject var vm = TestClass.shared // << observer
var body: some View {
Toggle(isOn: $vm.onStates[0]) { // << binding via observer
Text("Example Toggle")
}
}
When I click a button in ContentView.swift, I expect the action triggering an 'append' to a sportList would show the newly added item.. but it doesn't. No compile errors. Clicking on the button does nothing (even though I can see the SportItem being packaged up correctly) in a print statement.
I created a model for an item "SportItem" that has simple properties (e.g. name: String) and a ObservableObject class.
struct SportItem: Identifiable {
let name: String
}
I then create a global sportData variable and an ObservableObject class outside of everything:
var sportData = [
SportItem(name: "Tennis"),
SportItem(name: "Basketball")
]
class SportList: ObservableObject {
#Published var sportList: [SportItem]
init() {
self.sportList = sportData
}
}
In SportListView.swift, I have inside of the body:
#ObservedObject var sportList: SportList = SportList();
...
ForEach(sportList.sportList) {
sport in
VStack(alignment: .leading) {
Text(sport.name)
}
}
SportListView is referenced in ContentView.swift, which has:
var sportList:[SportItem] = sportItems
that SportList() using:
SportListView(sportList: SportList())
I also have a button in the parent ContentView.swift file where I have a button where the action of it performs a:
SportList().sportData.append(SportItem(name: "Soccer"))
When I click on that button, I notice the SportListView in the simulator does not add the new item. How do I get the list to be updated to show "Soccer" added onto the list?
You should keep sportData inside SportList (if needed via shared instance), like
class SportList: ObservableObject {
static let shared = SportList()
#Published var sportData: [SportItem] = [ SportItem(name: "Tennis"), SportItem(name: "Basketball") ]
}
and then
#ObservedObject var sportList: SportList = SportList.shared
...
ForEach(sportList.sportData) {
sport in
VStack(alignment: .leading) {
Text(sport.name)
}
}
and add like (if it is somewhere externally of view)
SportList.shared.sportData.append(SportItem(name: "Soccer"))
I'm learning SwiftUI at the moment. I've been playing around with loading a list from CoreData and making changes on / filtering etc. I've run into the issues below. Essentially as soon as I try to apply any conditionals within the ForEach I I'm presented with that error.
This works if I run iterate through the organisations in List itself rather than a ForEach. This isn't the ideal solution as I loose the inbuilt deletion function.
Am I missing something stupid?
let defaults = UserDefaults.standard
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Organisation.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Organisation.name, ascending: true)])
var orgs: FetchedResults<Organisation>
var body: some View
{
NavigationView {
List {
ForEach(orgs, id: \.self) {org in
if !self.userData.showFavsOnly || org.isFavorite {
NavigationLink(destination: OrganisationView(org: org, moc: self.managedObjectContext)) {
OrganisationRow(org: org)
}
}
}
}
}
}
There error code I get is I get is on the for each line and is
Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
Thanks for your help
Type '()' cannot conform to 'View'; only struct/enum/class types can
conform to protocols
This error means that your ForEach loop expects some View. But you give it an if-statement instead. What if the condition returns false?
The solution may be to wrap the if-statement in some View - it could be a Group, VStack, ZStack...
ForEach(orgs, id: \.self) { org in
Group {
if !self.userData.showFavsOnly || org.isFavorite {
NavigationLink(destination: OrganisationView(org: org, moc: self.managedObjectContext)) {
OrganisationRow(org: org)
}
}
}
}
I'm modelling view state in my viewModel using an enum...
enum ViewState<T> {
case idle
case error(Error)
case loading
case data([T])
I have a computed property to get the data
var data: [T] {
guard case let .data(data) = self else {
return []
}
return data
}
In one of my views I iterate through the data
var dropdownListView: some View {
ForEach(viewModel.state.data.indices, id: \.self) { index in
DropdownView(
viewModel: $viewModel.state.data[index],
isActionSheetPresented: $viewModel.isActionSheetPresented
)
}.eraseToAnyView()
}
I get an error as you can't make a binding from a computed property so make my own custom binding...
ForEach(viewModel.state.data.indicies, id: \.self) { index in
DropdownView(viewModel: Binding<ItemViewModel>(
get: {return viewModel.state.data[index] },
set: { value in
var data = viewModel.state.data
data[index] = value
viewModel.state = .data(data)
},
isActionSheetPresented: $viewModel.isActionSheetPresented
)
}
This works but are there any issues with setting the whole state again in the binding setter (I believe SwiftUI is intelligent enough that this would be efficient) or is there another way to do this here?
On my vision you mixed a state and a data, which are different things. So instead of .data([T]), I would recommend something like .loaded (ie, state) and keep data by standalone #Published var data: [T] property. If that adapted your code will look much more naturally.
Like
ForEach(viewModel.data.indices, id: \.self) { index in
DropdownView(
viewModel: $viewModel.data[index],
isActionSheetPresented: $viewModel.isActionSheetPresented
)
}//.eraseToAnyView() // << you don't need this
}
I'm trying to build an app with SwiftUI, and I'm just getting started with Combine framework. My first simple problem is that I'd like a single variable that defines when the app has been properly initialized. I'd like it to be driven by some nested objects, though. For example, the app is initialized when the account object is initialized, the project object is initialized, etc. My app could then use GlobalAppState.isInitialized, instead of inspected each nested object.
class GlobalAppState: ObservableObject {
#Published var account: Account = Account()
#Published var project: Project = Project()
#Published var isInitialized: Bool {
return self.account.initialized && self.project.initialized;
}
}
I get the error Property wrapper cannot be applied to a computed property
So...clearly, this is currently disallowed. Is there a way I can work around this??? I'd like to be able to use GlobalAppState.initialized as a flag in the app. More to the point, something like GlobalAppState.project.currentProject, which would be a computed property returning the currently selected project, etc...
I can see this pattern being used in a thousand different places! Any help would be wildly appreciated...
Thanks!
In this case there's no reason to use #Published for the isInialized property since it's derived from two other Published properties.
var isInitialized: Bool {
return self.account.initialized && self.project.initialized;
}
Here is one case if both account and project are structures.
struct Account{
var initialized : Bool = false
}
struct Project{
var initialized : Bool = false
}
class GlobalAppState: ObservableObject {
#Published var account: Account = Account()
#Published var project: Project = Project()
#Published var isInitialized: Bool = false
var cancellabel: AnyCancellable?
init(){
cancellabel = Publishers.CombineLatest($account, $project).receive(on: RunLoop.main).map{
return ($0.0.initialized && $0.1.initialized)
}.eraseToAnyPublisher().assign(to: \GlobalAppState.isInitialized, on: self) as AnyCancellable
}
}
struct GlobalAppStateView: View {
#ObservedObject var globalAppState = GlobalAppState()
var body: some View {
Group{
Text(String(globalAppState.isInitialized))
Button(action: { self.globalAppState.account.initialized.toggle()}){ Text("toggle Account init")}
Button(action: { self.globalAppState.project.initialized.toggle()}){Text("toggle Project init")}
}
}
}