0

I'm totally new to Swift. I need to traverse an array, and based on what the data is, build out a custom tree structure. I would share my code but it's too bulky and too horrible to be seen in public. It's not even close to working. So here's some pseudocode instead:

pass in an array of ScheduleCell models (data for a scheduled tv program) to a SwiftUI View.

Each ScheduleCell contains:
   Date of broadcast
   Channel
   Starttime
   Program title, subtitle, etc

in the view body, loop over each ScheduleCell
   if ScheduleCell is new day then
      display new date
      go down one line
   end
      
   if ScheduleCell is new channel then
      display ScheduleCell.channel but do NOT drop down a line
   end

   display ScheduleCell.title, also on same line as channel

   Keep going on the same horizontal line till you hit the next channel

end

The end effect should be like this:

11-16-2021

CNN         News Program 1    News Program 2    News Program 3        Etc.
NBC         Late Show         Infomercial       Barney Miller Rerun.  Etc.

11-17-2021

CNN         News Program 1    News Program 2    News Program 3        Etc.
NBC         Late Show         Infomercial       Barney Miller Rerun.  Etc.

It seems like it should be really simple to do this. I've tried setting up a tree structure, but I'm having trouble with the SwiftUI View protocol. I have tried building the tree structure in a function outside of the view body, but I'm having trouble figuring out how to get that structure into the view body.

In the alternative, I've backed off all that to just trying to get it to work via brute force inside the view by tracking the array index, like this:

struct ScheduleDisplayView: View {
    
    var schedule: [SchedSlot]
    let dfdo = DateFormatter.dateOnly
    let dfto = DateFormatter.timeOnly

    let chanwidth: CGFloat = 45.0
    let fontsize: CGFloat = 10.0

    var body: some View {
        
        List {
            ForEach(schedule.indices,id:\.self) { idx in
                let ssDay: String = dfdo.string(from: schedule[idx].startTime!)
                let ssChan: Int32 = schedule[idx].channel!.channelID
                if idx == 0 ||
                    ssDay != dfdo.string(from: schedule[idx-1].startTime!)
                {
                    VStack {
                        Text(ssDay)
                            .frame(maxWidth: 200 + chanwidth, alignment: .leading)
                    }
                }
                HStack {
                    if idx == 0 ||
                        ssChan != schedule[idx-1].channel!.channelID
                    {
                        VStack {
                            Text(String(schedule[idx].channel!.channelID))
                                .frame(maxWidth: chanwidth, alignment: .center)
                            //.font(.system(size: fontsize))
                            Text(schedule[idx].channel!.callSign!)
                                .frame(maxWidth: chanwidth, alignment: .center)
                            //.font(.system(size: fontsize))
                        }
                    }
                    Text(schedule[idx].program!.title!)
                        .frame(maxWidth: 200, alignment: .leading)
                        .border(Color.black)
                }
                
            }
        }
    }
}

But the above approach isn't working because the HStack can't keep the program titles on the same line.

Thanks in advance for any input.

Additional material:

Here's a few random slices of the CoreData entities this is based on:

SchedCell
stationID   starttime   programID   duration    endtime isUsed channel  program
34916   2021-09-29 19:09:00.000 EP000045050088  PT00H09M    2021-09-29 19:18:00.000 0
12131   2021-09-29 19:15:00.000 EP022222100024  PT00H15M    2021-09-29 19:30:00.000 0
34916   2021-09-29 19:18:00.000 EP000045050208  PT00H09M    2021-09-29 19:27:00.000 0

Program
series  programID   title   subtitle    fulldescription genre isUsed 
EP00000066  EP000000660001  A Pup Named Scooby-Doo  Night of the Living Burger  After a quarrel, a burgerlike creature haunts Shaggy and Scooby.    Children    0
EP00000066  EP000000660002  A Pup Named Scooby-Doo  For Letter or Worse The ghost of a long-dead gangster haunts a TV studio.   Children    0
EP00000066  EP000000660003  A Pup Named Scooby-Doo  A Bicycle Built for Boo!    A green monster steals Shaggy's bike.   Children    0
EP00000066  EP000000660004  A Pup Named Scooby-Doo  The Baby Sitter From Beyond The baby sitter for Shaggy's little sister appears to be a monster. Children    0

Channel
stationID callSign  fullName channelID isUsed
15722   WCIX    WCIX    2   0
11345   WCIA    WCIA    3   0
11278   WAND    WAND    4   0
10685   KSDK    KSDK    5   0
10269   HSN Home Shopping Network   6   0
11824   WRSP    WRSP    7   0
11069   QVC QVC 8   0

As for code samples of my attempts to build the tree structure outside of the view, I have no working code. It's all just fragments that produced a variety of error messages. Here are the node structures. I'm still working out a routine to assemble them into a working tree, will post that as soon as I have something worth looking at:

class RootNode {
    var children: [DayNode] = []
    
    func add(child: DayNode) {
      children.append(child)
      child.parent = self
    }
}

class DayNode {
    var parent: RootNode
    var date: String
    var children: [ChannelNode] = []
    
    init(date: String) {
        self.date = date
    }
    
    func add(child: ChannelNode) {
      children.append(child)
      child.parent = self
    }
}

class ChannelNode {
    var parent: DayNode
    var channel: String
    var children: [SchedSlot] = []
    
    init(channel: String) {
        self.channel = channel
    }
    
    func add(child: SchedSlot) {
      children.append(child)
      //child.parent = self
    }
}
Joe Bell
  • 13
  • 3
  • My advice is to start as simple as possible, probably with *new* code and see how far you can get before you get stuck. Then, include that here. Otherwise, with the question as-is, it's basically just a request for someone else to write the code for you, which is unlikely to get a good response here on SO. – jnpdx Nov 17 '21 at 02:01
  • PS -- `ctrl-i` will format your code in Xcode. – jnpdx Nov 17 '21 at 02:02
  • @jnpdx, ok, I was able to insert my latest code. It is exactly that, a seriously simplified version of the original. The project is simply too big to dump all the code in here. I'm not asking for anyone to write code for me. I just have no idea how swift wants me to do this. It can't be this hard. I've been struggling with this for over a week. Lots of dead code. I just need an idea, a concept, maybe a pointer to the right resource. That's all. I just can't figure out where to write the code that puts the tree together. SwiftUI View protocol is so restrictive I can't get it to work. – Joe Bell Nov 17 '21 at 04:47
  • I can write more of an answer later, but my initial impression is that trying to do this all via the *view* layout is a questionable plan. It seems like *sorting* the data by channel and date first is the right way to go. Can you provide some code with a sample data array? – jnpdx Nov 17 '21 at 04:58
  • How does the data come in from the API? Is it sorted? In other words, can you expect it to be in date order? In channel order? – jnpdx Nov 17 '21 at 07:01
  • Right now I'm loading the data from text files. Eventually it will come from the SchedulesDirect API. I'm sorting the text file data (first by station ID (not quite the same as channel), then by starttime), but subsorting on the channel number has been a problem, haven't got that to work with the NSSortDescriptor because it's not directly part of the SchedSlot entity, it's hidden in the associated Channel entity. So instead I'm doing that part of the sort on the ForEach, per my sample code. This would all be so much easier with some old-fashioned SQL. I hate object graphs. – Joe Bell Nov 17 '21 at 13:45

1 Answers1

0

As I mentioned in the comments, I think this is more of a data organization/sorting issue than a SwiftUI layout issue. If you data is grouped and sorted correctly, the layout becomes more trivial (eg you don't have to try to decide whether to break a line because of a new channel).

In the following example, I spend the bulk of the code grouping and sorting the data. Then, the layout itself is relatively simple.

It's important to note that I do a couple of unsafe things here for brevity, like using first! that you would want to test for or have contingencies for in real code.

struct SchedCell {
    var stationID: Int
    var startTime: Date
    var programID: String
    var channel: String
}

func generateSampleCells() -> [SchedCell] {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
    return [
        ("2021-09-29 19:09:00.000",34916,"EP000045050088","PT00H09M"),
        ("2021-09-29 19:09:00.000",34917,"EP000045050088","PT00H09M"),
        ("2021-09-29 19:15:00.000",12131,"EP022222100024","PT00H15M"),
        ("2021-09-29 19:18:00.000",34916,"EP000045050208","PT00H09M"),
        ("2021-09-30 19:09:00.000",34916,"EP000045050088","PT00H09M"),
        ("2021-09-30 19:15:00.000",12131,"EP022222100024","PT00H15M"),
        ("2021-09-30 19:18:00.000",34916,"EP000045050208","PT00H09M"),
        ("2021-09-30 19:15:00.000",12132,"EP022222100024","PT00H15M"),
        ("2021-09-29 19:09:00.000",4916,"EP000045050088","PT00H09M"),
        ("2021-09-29 19:09:00.000",4917,"EP000045050088","PT00H09M"),
        ("2021-09-29 19:15:00.000",2131,"EP022222100024","PT00H15M"),
    ].map {
        SchedCell(stationID: $0.1, startTime: formatter.date(from: $0.0)!, programID: $0.2, channel: $0.3)
    }
}

struct ContentView: View {
    
    private var data = generateSampleCells()
    private var formatter = DateFormatter()
    
    struct ScheduleDay {
        var dateStamp: String
        var date : Date
        var channels: [ChannelLineup]
    }

    struct ChannelLineup {
        var channelName: String
        var programs: [SchedCell]
    }

    struct DateStampedSchedCell {
        var dateStamp: String
        var cell: SchedCell
    }
    
    var sortedData : [ScheduleDay] {
        formatter.dateFormat = "MM-dd-yyyy"

        let dateStamped = data
            .map { item -> DateStampedSchedCell in
            DateStampedSchedCell(dateStamp: formatter.string(from: item.startTime), cell: item)
        }

        let days = Dictionary(grouping: dateStamped, by: { $0.dateStamp} ).values

        let channelMappedDays = days.map { day in
            ScheduleDay(dateStamp: day.first!.dateStamp,
                        date: day.first!.cell.startTime,
                        channels: Dictionary(grouping: day, by: { $0.cell.channel }).map { ChannelLineup(channelName: $0.key, programs: $0.value.map(\.cell))}
            )
        }.sorted(by: {a,b in a.date < b.date})

        return channelMappedDays
    }
    
    var body: some View {
        ScrollView(.horizontal) {
            ForEach(sortedData, id: \.dateStamp) { day in
                VStack(alignment: .leading) {
                    Text("Day: \(day.dateStamp)")
                        .bold()
                    ForEach(day.channels, id: \.channelName) { channel in
                        HStack {
                            Text("Channel: \(channel.channelName)")
                                .foregroundColor(.red)
                            ForEach(channel.programs, id: \.programID) { program in
                                Text(program.programID)
                            }
                        }
                    }
                }.frame(maxWidth: .infinity, alignment: .leading)
            }
        }
    }
}

enter image description here

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • wow, this looks great! Though I will have to study it a lot to fully understand it. I haven't done anything with maps yet, so I'm going to have to really pick apart what's going on in the sortedData var. But what's cool is how easy this plugs into the view. Again, I'm not used to seeing this sort of approach. I came up through the ranks as an old-school Fortran programmer. Made the jump to VS C#, but never got fully onboard with MVC, so these damn restrictive protocols are throwing me for a loop. Yes, the pun was intended lol. :) Thanks so much for your help! – Joe Bell Nov 17 '21 at 14:00