I have a time selection subview with two wheel pickers, one for minutes and one for seconds. The values are bound to two (local) state variables in the parent view. I'm looking for a way to have a global state variable "time" changed every time when either one of the two local variables changes. I don't want to directly bind the two state variables for min/sec to the global state as I want only one variable there holding the time in seconds only. So there should be a conversion time = min * 60 + seconds.
I'm struggling to find a way of detecting a value change of the two local state variables.
Here's the subview:
struct TimePicker: View {
var min: Binding<Int>
var sec: Binding<Int>
var body: some View {
HStack() {
Spacer()
Picker(selection: min, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Min.")
Picker(selection: sec, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Sec.")
Spacer()
}
}
}
You may use combineLatest in the main view.
struct MainTimeView: View{
#State private var min : Int = 0
#State private var sec : Int = 0
#State private var myTime: Int = 0
var body: some View {
TimePicker(min: $min, sec: $sec).onReceive(Publishers.CombineLatest(Just(min), Just(sec))) {
self.myTime = $0 * 60 + $1
print(self.myTime)
}
}
}
i think you need to wrapped your global variable kinda
class TimeObject : BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var time:Int = 0 {
didSet {
didChange.send(())
}
}
}
...
struct TimePicker: View {
var min: Binding<Int>
var sec: Binding<Int>
#EnvironmentObject var time : TimeObject
var body: some View {
HStack() {
Spacer()
Picker(selection: min, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}.gesture(TapGesture().onEnded(){
time.time = ix
})
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Min.")
Picker(selection: sec, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}.gesture(TapGesture().onEnded(){
time.time = ix
})
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Sec.")
Spacer()
}
}
}
...
TimePicker..environmentObject(TimeObject())
unfortunately don't have a chance to test
Related
I am still quite new at SwiftUI and I am having trouble to understand why the array that I have created in my model “GameSession”, which is sorted according to a computed variable defined in my second model “GamePlayer”, is not “always” correctly updated.
I have simplified my code to make it more readable, in order to maximize the chances of someone helping me!
Description of my code:
In my App, I have 2 entities
1. GameSession :which has Attributes "date", "place", "nbplayer" + a relationship called "players" with " GamePlayer "
2. GamePlayer: which has Attributes "name","scorelevel1","scorelevel2","scorelevel3" + a relationship with "GameSession"
the relationship is "one to many": One GameSession can have many GamePlayer
And basically, I have 3 views:
View 1: Just one navigation link which brings you to View2
View2: Display a list of “Game” played (on which you can click to go to “view3”). For each one, I want to be able to display the date of the game, and the names of the players who played the game, sorted according to a variable “total_score”
View3: You can visualize the game on which you clicked in View2. And you see the name of the players who played the game, and you can modify the score of each player for each one of the 3 levels. And a variable “total_score” sums for each player the score of level 1 to 3
My problem:
My issue, is that when I go from View3 to View2, the list of the players won’t be correctly sorted according to the “total_score” variable.
But If I go from View1 to View2, the list of the player is correctly sorted…
I don’t understand what I am doing wrong.
Here is my code:
My Model for GameSession is like that:. You will notice that I have created in my model a variable which is an array of “GamePlayer” sorted according to the computed variable “total_score” of the GamePlayer entity
import Foundation
import CoreData
extension GameSession {
#nonobjc public class func fetchRequest() -> NSFetchRequest<GameSession> {
return NSFetchRequest<GameSession>(entityName: "GameSession")
}
#NSManaged public var date: Date?
#NSManaged public var nbplayer: Int32
#NSManaged public var place: String?
#NSManaged public var players: NSSet?
#NSManaged public var photo: NSData?
// pour gerer le fait que "date" ne doit pas etre vide
public var wrappedDate: Date {
date ?? Date()
}
public var wrappedNbplayer: Int32 {
nbplayer
}
// pour gerer le fait que "lieu" ne doit pas etre vide
public var wrappedPlace: String {
place ?? "Endroit inconnu" }
// pour gerer le fait que "photo" ne doit pas etre vide
public var wrappedPhoto: NSData {
photo ?? NSData()
}
}
//
public var playersArrayRang: [GamePlayer] {
let playersSet = players as? Set< GamePlayer > ?? []
// On retourne un array sorted en fonction du score total
return playersSet.sorted {
$0.totalscore < $1.totalscore
}
}
And my model for “GamePlayer” is like that:
extension GamePlayer {
#nonobjc public class func fetchRequest() -> NSFetchRequest< GamePlayer > {
return NSFetchRequest< GamePlayer >(entityName: " GamePlayer ")
}
#NSManaged public var name: String?
#NSManaged public var scorelevel1: Int64
#NSManaged public var scorelevel2: Int64
#NSManaged public var scorelevel3: Int64
#NSManaged public var sessiongame: GameSession?
public var wrappedName: String {
name ?? "player unknown"
}
public var total_score: Int64 {
scorelevel1 + scorelevel2 + scorelevel3
}
}
View 1: GameUIView
struct GameUIView: View {
var body: some View {
NavigationLink(destination: GameListePartieUIView ()) {
Text(" Mes parties ")
}
}
}
View 2: GameListePartieUIView
import SwiftUI
import Foundation
struct GameListePartieUIView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: GameSession.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \GameSession.date, ascending: false)
]) var gamesessions: FetchedResults<GameSession>
#State var showingAddPartie = false
var body: some View {
// VStack {
List {
ForEach(gamesessions, id: \.date) { session in
HStack{
NavigationLink (destination: DetailPartieSelecUIView2(session: session)){
HStack{
Text(" \(session.wrappedDate, formatter: itemFormatter) ")
.frame(width: 120, height: 50, alignment: .top)
.font(.system(size: 12, weight: .light, design: .serif))
VStack(alignment: .leading){
ForEach(Array(session.playersArrayRang.enumerated()), id: \.offset) { index, player in
if index == 0 {
Text("\(index+1) - \(player.wrappedName)")
.frame(alignment: .leading)
.lineLimit(1)
.font(.system(size: 12, weight: .light, design: .serif))
// .foregroundColor(Color(red: 0.246, green: 0.605, blue: 0))
.foregroundColor(Color("Colorjb1"))
.bold()
} else {
Text("\(index+1) - \(player.wrappedName)")
.frame(alignment: .leading)
.lineLimit(1)
.font(.system(size: 12, weight: .light, design: .serif))
}
}
}
}
}
}
}
.onDelete(perform: deleteSessions)
.padding(1)
}
.navigationBarItems(trailing: Button("Ajouter") {
self.showingAddPartie.toggle()
})
.navigationBarTitle("Parties", displayMode: .inline)
.sheet(isPresented: $showingAddPartie) {
AddPartieRamiUIView().environment(\.managedObjectContext, self.moc)
}
.frame( maxWidth: .infinity)
.edgesIgnoringSafeArea(.all)
.listStyle(GroupedListStyle()) // or PlainListStyle()
.padding(1)
}
// --------
private func deleteSessions(at offsets: IndexSet) {
for index in offsets {
let session = ramisessions[index]
moc.delete(session)
}
do {
try moc.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
View 3: DetailPartieSelecUIView2
struct DetailPartieSelecUIView2: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest
private var players: FetchedResults<GamePlayer>
private var session_actuelle: GameSession
init(session: GameSession) {
let predicate = NSPredicate(format: "sessionramy = %#", session)
let sortDescriptors = [SortDescriptor(\GamePlayer.name)] // need something to sort by.
_players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
session_actuelle = session
}
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
var body: some View {
VStack {
ScrollView(.vertical){
Text("\(session_actuelle.wrappedLieu) - \(session_actuelle.wrappedDate, formatter: itemFormatter)")
ScrollView([.horizontal, .vertical]) {
HStack(alignment:.center){
// 1er element de mon HStack
VStack {
Text(" ")
Divider()
Text("Level 1")
Text("Level 2")
Text("Level 3")
Divider()
Text("Total")
}
// 2eme element de mon HStack
ForEach(players, id: \.wrappedName) { player in
PlayerView(player: player)
}
} // HStack
} //scrollView
// bouton pour sauvegarder les scores de la partie
Button("Save scores") {
do {
try self.moc.save()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}
}
}
}
}
struct PlayerView: View {
#ObservedObject var player:GamePlayer
#Environment(\.managedObjectContext) var moc
#State private var numberFormatter: NumberFormatter = {
var nf = NumberFormatter()
nf.numberStyle = .decimal
return nf
}()
var body: some View {
VStack {
Text(player.wrappedName)
Divider()
TextField("Score", value: $player.scorelevel1, formatter:NumberFormatter())
TextField("Score", value: $player.scorelevel2, formatter:NumberFormatter())
TextField("Score", value: $player.scorelevel3, formatter:NumberFormatter())
Divider()
Text(String(player.scoretotal))
}
}
}
extension Binding {
func toUnwrapped<T>(defaultValue: T) -> Binding<T> where Value == Optional<T> {
Binding<T>(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 })
}
}
When I am in my “View3” and I change values in the TextField (of PlayerView), player.scoretotal is updated accordingly.
Then I click on my button "Save scores", in order to save my “moc” (Core Data)
But when I click on “back” (on the top left corner of the view) and that I go back to the previous view (View2), the right part of my list, where I am supposed to sort players name according to their total_score, is not updated…
What is weird, is that, if I go “back” to my “View1”, and that I click on the navigation link which open my “View2” (DetailPartieSelecUIView2), Then the names of the players are sorted correctly…
Thanks a lot for reading my long post. Hopefully somebody can help me. Best regards!
I tried everything...
These lines are mistakes:
ForEach(gamesessions, id: \.date) { session in
NSManagedObjects implement the Identifiable protocol so no id param is needed and you wouldn't want to use a date for identity anyway.
ForEach(Array(session.playersArrayRang.enumerated()), id: \.offset) { index, player in
ForEach is a View not a for loop.
Try:
ForEach(gamesessions) { session in
ForEach(players) { player in
If you are trying to make a scoreboard then you need another entity in your model, e.g. ScoreboardEntry with an int rank and when you save any player you need to re-order these entries and assign the rank. You could also use the new derived attributes feature to automate this. Then you can make a FetchRequest that sorts by rank.
I am creating a basic manual counter to keep track of visitors, however, I am struggling to find out how I can capture and present the previous value entered by the user alongside the current number.
When I tap on the counter (0) (aka editednumber), a box appears and the user is asked to enter a number, I want to be able to save the number entered by the user, so when the user taps the counter again to enter a new number, the screen will show the previous number entered as well as the current number entered.
The previous number will of course be overwritten, every time a new number is entered, but regardless, I would like the previous number and new number to appear.
Example:
User enters the number 10, this will show as current_guests/editednumber which is fine, but if I tap to enter a new number 12, only the last entered number (10) is showing.
I want the view to show both the old (10) (stored into the previous_editednumber variable) and current (12) number (editednumber).
My code is as following:
// testView.swift
import SwiftUI
struct testView: View {
#State var current_guests:Int = 0
#State var denied_guests:Int = 0
#State var total_guests:Int = 0
#State var editednumber:Int = 0
#State var previous_editednumber:Int = 0
#State private var presentAlert = false
var body: some View {
VStack {
VStack {
Text("total_guests: \(total_guests)")
Text("current_guests: \(current_guests)")
Text("editednumber: \(editednumber)")
Text("previous_editednumber:\(previous_editednumber)")
Button("\(current_guests)") {
presentAlert = true
}
.alert("", isPresented: $presentAlert, actions: {
TextField("Number", value: $editednumber, formatter: NumberFormatter()).font(.system(size: 18)).foregroundColor(.black).multilineTextAlignment(.center).keyboardType(.numberPad)
Button("OK", action: {
// perform calculations based on input
if (editednumber >= total_guests) {
current_guests = editednumber
total_guests = editednumber + total_guests
}
if (editednumber < total_guests) {
current_guests = editednumber
total_guests = total_guests - current_guests
}
})
Button("Cancel", role: .cancel, action: {})
}, message: {
Text("Enter number of guests inside")
}).font(.system(size: 58, weight: .heavy)).keyboardType(.decimalPad) .frame(maxWidth: .infinity, alignment: .center).padding(.bottom,70).ignoresSafeArea(.keyboard)
}
// main buttons
HStack {
Button {
current_guests += 1
total_guests += 1
}label: {
Image(systemName: "plus")}.foregroundColor(.white).background(Color .green).frame(width: 80, height: 80).background(Color.green).font(.system(size: 50)).cornerRadius(40).padding()
Button {
denied_guests += 1
}label: {
Image(systemName: "nosign")}.foregroundColor(.white).background(Color .orange).frame(width: 80, height: 80).background(Color.orange).font(.system(size: 50)).cornerRadius(40).padding()
Button {
current_guests -= 1
if (current_guests <= 0) {
current_guests = 0
} }label: {
Image(systemName: "minus")}.foregroundColor(.white).background(Color .red).frame(width: 80, height: 80).background(Color.red).font(.system(size: 50)).cornerRadius(40).padding()
}
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
You can create an in-between #State that takes its initial value from editednumber then returns the newValue when the user clicks "Ok".
struct AlertBody: View{
#State var newValue: Int
let onAccept: (Int) -> Void
let onCancel: () -> Void
init(initialValue: Int, onAccept: #escaping (Int) -> Void, onCancel: #escaping () -> Void){
self._newValue = State(initialValue: initialValue)
self.onAccept = onAccept
self.onCancel = onCancel
}
var body: some View{
TextField("Number", value: $newValue, formatter: NumberFormatter()).font(.system(size: 18)).foregroundColor(.black).multilineTextAlignment(.center).keyboardType(.numberPad)
Button("OK", action: {
onAccept(newValue)
})
Button("Cancel", role: .cancel, action: onCancel)
}
}
Then you can replace the content in the alert
struct HistoryView: View {
#State private var currentGuests:Int = 0
#State private var deniedGuests:Int = 0
#State private var totalGuests:Int = 0
#State private var editedNumber:Int = 0
#State private var previousEditedNumber:Int = 0
#State private var presentAlert = false
var body: some View {
VStack {
VStack {
Text("total_guests: \(totalGuests)")
Text("current_guests: \(currentGuests)")
Text("editednumber: \(editedNumber)")
Text("previous_editednumber:\(previousEditedNumber)")
Button("\(currentGuests)") {
presentAlert = true
}
.alert("", isPresented: $presentAlert, actions: {
AlertBody(initialValue: editedNumber) { newValue in
//Assign the previous number
previousEditedNumber = editedNumber
//Assign the newValue
editedNumber = newValue
//Your previous logic
if (editedNumber >= totalGuests) {
currentGuests = editedNumber
totalGuests = editedNumber + totalGuests
}
if (editedNumber < totalGuests) {
currentGuests = editedNumber
totalGuests = totalGuests - currentGuests
}
} onCancel: {
print("cancelled alert")
}
}, message: {
Text("Enter number of guests inside")
}).font(.system(size: 58, weight: .heavy)).keyboardType(.decimalPad) .frame(maxWidth: .infinity, alignment: .center).padding(.bottom,70).ignoresSafeArea(.keyboard)
}
// main buttons
HStack {
Button {
currentGuests += 1
totalGuests += 1
}label: {
Image(systemName: "plus")}.foregroundColor(.white).background(Color .green).frame(width: 80, height: 80).background(Color.green).font(.system(size: 50)).cornerRadius(40).padding()
Button {
deniedGuests += 1
}label: {
Image(systemName: "nosign")}.foregroundColor(.white).background(Color .orange).frame(width: 80, height: 80).background(Color.orange).font(.system(size: 50)).cornerRadius(40).padding()
Button {
currentGuests -= 1
if (currentGuests <= 0) {
currentGuests = 0
} }label: {
Image(systemName: "minus")}.foregroundColor(.white).background(Color .red).frame(width: 80, height: 80).background(Color.red).font(.system(size: 50)).cornerRadius(40).padding()
}
}
}
}
I have code like this:
List(datalist) { data in
NavigationLink(destination: View1(data: data).headless()) {
Text(data.name)
}
}
where headless() is a way to avoid all the default top views of a NavigationLink and its corresponding init:
extension View {
// https://www.hackingwithswift.com/forums/swiftui/removing-unwanted-and-unknown-whitespace-possibly-a-navigation-bar-above-a-view/7343
func headless() -> some View {
return self.navigationBarTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
What I want to do is have a View struct that can be a customized call to NavigationLink that always calls the headless() modifier. I have written this, copying from the declaration of NavigationLink:
struct SimpleNavLink<Destination, Label>: View where Label : View, Destination : View {
private let label: () -> Label
private let destination: () -> Destination
init(#ViewBuilder destination: #escaping () -> Destination, #ViewBuilder label: #escaping () -> Label) {
self.label = label
self.destination = destination
}
var body: some View {
NavigationLink(destination: destination().headless, label: label)
}
}
With that in place I changed the NavigationLink line to this:
SimpleNavLink(destination: View1(data: data)) {
But that gave me the error
Cannot convert value of type 'View1' to expected argument type '() -> Destination'
That was easy enough to solve by just wrapping the destination in { }:
SimpleNavLink(destination: { View1(data: data) } ) {
But WHY? I didn't have to do that for the NavigationLink. When I tried adding #autoclosure to the destination parameter, the compiler said that didn't go with #ViewBuilder
You said
I have written this, copying from the declaration of :
I assume you meant “the declaration of NavigationLink”. But your original code uses a NavigationLink.init that is declared like this:
#available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Pass a closure as the destination")
#available(macOS, introduced: 10.15, deprecated: 100000.0, message: "Pass a closure as the destination")
#available(tvOS, introduced: 13.0, deprecated: 100000.0, message: "Pass a closure as the destination")
#available(watchOS, introduced: 6.0, deprecated: 100000.0, message: "Pass a closure as the destination")
public init(destination: Destination, #ViewBuilder label: () -> Label)
This version of init takes the Destination by value instead of as a function. It's also going to be deprecated at some point.
So, if you want to mimic the (eventually deprecated) syntax, you need to change your init to take the Destination by value instead of as a function. Note also that NavigationLink does not require #escaping closures, so perhaps you shouldn't either. Thus:
struct SimpleNavLink<Destination: View, Label: View>: View {
private let label: Label
private let destination: Destination
init(
destination: Destination,
#ViewBuilder label: () -> Label
) {
self.label = label()
self.destination = destination
}
var body: some View {
NavigationLink(
destination: destination.headless(),
label: { label }
)
}
}
I have a segmented control that I am using as a tabs in the toolbar of my app linked to the selectedTab variable. when I switch tabs the accounts list changes but it does not reset the lines list. I tried using initialValue on the selected to make sure it reset to 0 but this did not effect it. I tried a print in the init to make sure that the value of selected was 0, after the init. it was every time but it still did not refresh the lines foreach list.
What am I missing?
import SwiftUI
import SQLite3
struct ContentView:View {
#EnvironmentObject var shared:SharedObject
var body: some View {
VStack {
if shared.selectedTab == 0 {
LedgerView(ledger: .Accounts)
} else if shared.selectedTab == 1 {
LedgerView(ledger: .Budgets)
} else if shared.selectedTab == 2 {
ReportsView()
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct LedgerView:View {
#EnvironmentObject var shared:SharedObject
let ledger:LedgerType
#State var selected:Int = 0
init(ledger:LedgerType) {
self.ledger = ledger
self._selected = State(initialValue: 0)
}
var body:some View {
HStack {
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.accounts.filter({$0.ledger == ledger})) { account in
Text(account.name)
.background(account.id == self.selected ? Color.accentColor : Color.clear)
.onTapGesture {self.selected = account.id}
}
}
Divider()
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.journalLines.filter({$0.accountID == selected})) { line in
Text("Line#\(line.id)")
}
}
}
}
}
struct ReportsView:View {
var body:some View {
Text("Under Construction ...")
}
}
class SharedObject:ObservableObject {
#Published var accounts:[Account] = []
#Published var journalLines:[JournalLine] = []
#Published var selectedTab:Int = 0
init() {
loadData()
}
}
enum LedgerType:Int {
case Accounts=0,Budgets=1
var name:String {
switch(self) {
case .Accounts: return "Accounts"
case .Budgets: return "Budgets"
}
}
}
struct Account:Identifiable {
var id:Int
var name:String
var ledger:LedgerType
}
struct Payee:Identifiable {
var id:Int
var name:String
}
struct Journal:Identifiable {
var id:Int
var date:Date
var payeeID:Int
var memo:String?
}
struct JournalLine:Identifiable {
var id:Int
var journalID:Int
var accountID:Int
var amount:Double
}
edit abridged demo code to try to isolate the problem
import SwiftUI
struct ContentView: View {
#EnvironmentObject var shared:SharedObject
var body: some View {
VStack {
Picker(selection: $shared.selectedTab, label: Text("")) {
Text("Accounts").tag(0)
Text("Budgets").tag(1)
}.pickerStyle(SegmentedPickerStyle())
Divider()
if shared.selectedTab == 0 || shared.selectedTab == 1 {
LedgerView()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct LedgerView:View {
#State var selected:Int = 0
init() {
self._selected = State(initialValue: 0)
print("LedgerView.init()")
}
var body:some View {
VStack(alignment: HorizontalAlignment.leading) {
Text("Selected: \(selected)")
Picker(selection: $selected, label: Text("")) {
Text("Account#1").tag(1)
Text("Account#2").tag(2)
Text("Account#3").tag(3)
}
}
}
}
class SharedObject: ObservableObject {
#Published var selectedTab:Int = 0
}
Based on your recent comment, what you need is #Binding and not #State. To explain: The struct LedgerView is just an extract of the picker in a separate view. When you call LedgerView(), this doesn't instantiate a new object. It simply adds the picker view in that place. Therefore, when you need the picker to reset on switching tabs, you need to use binding to reset the picker. Here's the working code. Hope it helps.
struct ContentView: View {
#EnvironmentObject var shared:SharedObject
#State var selected: Int = 0
var body: some View {
VStack {
Picker(selection: $shared.selectedTab, label: Text("")) {
Text("Accounts").tag(0)
Text("Budgets").tag(1)
}.pickerStyle(SegmentedPickerStyle())
Divider()
if shared.selectedTab == 0 || shared.selectedTab == 1 {
LedgerView(selected: $selected)
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(shared.$selectedTab) { newValue in
self.selected = 0
}
}
}
struct LedgerView:View {
#Binding var selected: Int
var body:some View {
VStack(alignment: HorizontalAlignment.leading) {
Text("Selected: \(selected)")
Picker(selection: $selected, label: Text("")) {
Text("Account#1").tag(1)
Text("Account#2").tag(2)
Text("Account#3").tag(3)
}
}
}
}
[Earlier answer containing alternate solution]
I've modified your code to make the lines work. I've added sample data to get the code to work. I doubt if the problem is with your data. Also I have modified the enum LedgerType to make it iterable. Here's the working code.
I've modified the following in code:
Removed passing ledgerType to LedgerView as the source of truth is
#EnvironmentObject var shared: SharedObject
Added code to select the first account by default when switching tabs. This refreshes the lines when switching between tabs. See the code in .onReceive
Hope this helps. If you need anything, let me know.
struct ContentView:View {
#EnvironmentObject var shared: SharedObject
var body: some View {
VStack {
Picker(selection: $shared.selectedTab, label: Text("")) {
ForEach(0 ..< LedgerType.allCases.count) { index in
Text(LedgerType.allCases[index].rawValue).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
if shared.selectedTab == 0 || shared.selectedTab == 1 {
LedgerView()
} else if shared.selectedTab == 2 {
ReportsView()
}
Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct LedgerView:View {
#EnvironmentObject var shared:SharedObject
#State var selected: Int = 0
var body:some View {
HStack {
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.accounts.filter({ $0.ledger == LedgerType.allCases[shared.selectedTab] })) { account in
Text(account.name)
.background(account.id == self.selected ? Color.accentColor : Color.clear)
.onTapGesture {self.selected = account.id}
}
}
Divider()
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.journalLines.filter({$0.accountID == selected})) { line in
Text("Line#\(line.id)")
}
}
}
.onReceive(shared.$selectedTab) { newValue in
if let id = self.shared.getInitialAccountId(tabIndex: newValue) {
self.selected = id
}
}
}
}
struct ReportsView:View {
var body:some View {
Text("Under Construction ...")
}
}
class SharedObject:ObservableObject {
#Published var accounts:[Account] = []
#Published var journalLines:[JournalLine] = []
#Published var selectedTab:Int = 0
func getInitialAccountId(tabIndex: Int) -> Int? {
if tabIndex == 0 {
return accounts.filter({
$0.ledger == LedgerType.Accounts
}).first?.id
}
else if tabIndex == 1 {
return accounts.filter({
$0.ledger == LedgerType.Budgets
}).first?.id
}
else {
return accounts.filter({
$0.ledger == LedgerType.Reports
}).first?.id
}
}
init() {
accounts = [
Account(id: 1, name: "Sales", ledger: .Accounts),
Account(id: 2, name: "Purchase", ledger: .Accounts),
Account(id: 3, name: "Forecast", ledger: .Budgets)
]
journalLines = [
// Line for sales
JournalLine(id: 1, journalID: 10, accountID: 1, amount: 200),
JournalLine(id: 2, journalID: 20, accountID: 1, amount: 400),
// Line for purchase
JournalLine(id: 3, journalID: 30, accountID: 2, amount: 600),
JournalLine(id: 4, journalID: 40, accountID: 2, amount: 800)
]
}
}
enum LedgerType: String, CaseIterable {
case Accounts = "Accounts"
case Budgets = "Budgets"
case Reports = "Reports"
}
struct Account:Identifiable {
var id:Int
var name:String
var ledger:LedgerType
}
struct Payee:Identifiable {
var id:Int
var name:String
}
struct Journal:Identifiable {
var id:Int
var date:Date
var payeeID:Int
var memo:String?
}
struct JournalLine:Identifiable {
var id:Int
var journalID:Int
var accountID:Int
var amount:Double
}
shared.accounts doesn't exist. you have never initialized it
[...]
if shared.selectedTab == 0 {
LedgerView(ledger: .Accounts).environmentObject(SharedObject())
} else if shared.selectedTab == 1 {
LedgerView(ledger: .Budgets).environmentObject(SharedObject())
[...]
Edit: Make sure your SceneDelgate also initiates SharedObject otherwise it wont work on physical/simulator device
--
Edit: After running your code and you confirmed you are loading, I ran your code with no change and I don't see any issue, can you look at the gif below and comment what's wrong in what I am seeing?
How can I add drag and drop to reorder rows on SwiftUI?
Just a clean solution without 'Edit mode'. Here an example:
UPDATE
I asked this question on The SwiftUI Lab and the author replied with this code. Only works on iPad
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
let name: String
let image: String
}
struct ContentView: View {
#State var selection: Set<UUID> = []
#State private var fruits = [
Fruit(name: "Apple", image: "apple"),
Fruit(name: "Banana", image: "banana"),
Fruit(name: "Grapes", image: "grapes"),
Fruit(name: "Peach", image: "peach"),
Fruit(name: "Kiwi", image: "kiwi"),
]
var body: some View {
VStack {
NavigationView {
List(selection: $selection) {
ForEach(fruits) { fruit in
HStack {
Image(fruit.image)
.resizable()
.frame(width: 30, height: 30)
Text(fruit.name)
}
}
.onMove { _, _ in }
}
.navigationBarTitle("Fruits (Top)")
}
}
}
}
To put the list in the edit mode when the user long presses an item, you can use a state flag and set the edit environment value accordingly. It is important to make the flag changes animated in order not to look very weird.
struct ContentView: View {
#State private var fruits = ["Apple", "Banana", "Mango"]
#State private var isEditable = false
var body: some View {
List {
ForEach(fruits, id: \.self) { user in
Text(user)
}
.onMove(perform: move)
.onLongPressGesture {
withAnimation {
self.isEditable = true
}
}
}
.environment(\.editMode, isEditable ? .constant(.active) : .constant(.inactive))
}
func move(from source: IndexSet, to destination: Int) {
fruits.move(fromOffsets: source, toOffset: destination)
withAnimation {
isEditable = false
}
}
}
Draggable items in SwiftUI List with changing the order
I solved it with return NSItemProvider() when I try to drag an item.
And standard .onMove function.
If I understand correctly, I am grabbing a row container as an NSObject (thanks to initialization NSItemProvider), and .OnMove allows me the ability to reorder items in the List.
I'm still learning and may misunderstand some of the nuances. There must be a better explanation. But it definitely works (I only tested this on ios 15 because I use the .background property in my project).
// View
List {
ForEach(tasks) { task in
HStack { // Container of a row
NavigationLink {
Text("There will be an editing View")
} label: {
TaskListRowView(task: task)
}
}
.onDrag { // mean drag a row container
return NSItemProvider()
}
}
.onDelete(perform: deleteItems)
.onMove(perform: move)
}
// Function
func move(from source: IndexSet, to destination: Int) {
tasks.move(fromOffsets: source, toOffset: destination )
}
There is a great writeup on this at: https://www.hackingwithswift.com/quick-start/swiftui/how-to-let-users-move-rows-in-a-list
I don't want to copy paste Paul's code directly here so here's another example I wrote myself with a slightly different structure.
import SwiftUI
struct ReorderingView: View {
#State private var items = Array((0...10))
var body: some View {
NavigationView {
VStack {
List {
ForEach(items) { item in
Text("\(item)")
}.onMove { (source: IndexSet, destination: Int) -> Void in
self.items.move(fromOffsets: source, toOffset: destination)
}
}
}
//.environment(\.editMode, .constant(.active)) // use this to keep it editable always
.navigationBarItems(trailing: EditButton()) // can replace with the above
.navigationBarTitle("Reorder")
}
}
}
struct ReorderingView_Previews: PreviewProvider {
static var previews: some View {
ReorderingView()
}
}
extension Int: Identifiable { // Please don't use this in production
public var id: Int { return self }
}