tsonobe1 / imonit

0 stars 0 forks source link

時間帯が重なるTaskのTaskBoxのwidthを可変にする #56

Closed tsonobe1 closed 1 year ago

tsonobe1 commented 1 year ago
スクリーンショット 2022-11-20 11 55 07

↑ 2つのboxが重なっている.

これらのwitdhを左右に分割して、重ならないようにする

tsonobe1 commented 1 year ago

Splittable task box

tsonobe1 commented 1 year ago

重複している状態を持っていることを判定する 参考:https://cpoint-lab.co.jp/article/202103/19357/

tsonobe1 commented 1 year ago

forEachで前後の要素にアクセスする https://stackoverflow.com/questions/60642206/how-to-get-the-previous-element-in-a-swiftui-foreach

https://stackoverflow.com/questions/49476485/swift-loop-over-array-elements-and-access-previous-and-next-elements

tsonobe1 commented 1 year ago

idea 1

イメージ : https://www.hrupin.com/2017/04/calendar-dayview-for-android 実装 : https://stackoverflow.com/questions/53215825/return-optimized-x-coordinates-to-normalize-maximize-area-for-an-array-of-rectan

EventをRegionに区別する

Eventのリストをループさせて、

 y: Height (StartDate)
 type: START
 index: Index
 y: Height (EndDate)
 type: END
 index: index

というデータを作成。= EventをStartDateとEndDateの2つに分ける STARTとENDを別データにすることで、後々重複箇所を判定するのに役立つ

これらを、eventQueueに突っ込む。 eventQueueはpriority Queue。

Queueの優先順位は、 y -> Type -> IDの順。

TaskのStartDateを先にカウントしていることで、いくつのTaskがOverlapしているのかを数えることができる。

STARTが連続するたび、overlap変数に加算++していき、それをmaxOverlapにコピーする。 ENDが来たら、overlapを--していき、0になったらoverlapしている要素が終わったということになる。 overlap--の処理の際は、maxOverlapは上書きしない

Startが加算を開始してから、overlap0に戻るまでの要素を、1つのRegionとして、regionQueueにenqueueしていく。 RegionにはmaxOverlapプロパティがある。

RegionをForで回して描画していく

eventのwidthは、dailyCalenderのwidth / maxOverlapを指定することで、eventを重複せずに表示することができる

必要な知識

Priority Queue :

tsonobe1 commented 1 year ago

参考: https://stackoverflow.com/questions/11311410/visualization-of-calendar-events-algorithm-to-layout-events-with-maximum-width

idea 2

tsonobe1 commented 1 year ago

参考:https://stackoverflow.com/questions/14484843/custom-calendar-layout-collision-detection

idea3

tsonobe1 commented 1 year ago

解決

import Foundation
import SwiftUI

// MARK: Model
// CoreDataのEntityの再現
struct Task1 {
    var name: String
    var startDate: Date
    var endDate: Date
    var id: UUID
}

// Task1をStartDateとEndDateに分割するための構造体
struct TimeMoment {
    var time: Date
    var type: TimeType
    var index: Int
    var id: UUID

    enum TimeType: Comparable {
        case START
        case END

        // 比較演算子を使用可能にする
        static func < (lhs: TimeType, rhs: TimeType) -> Bool {
            switch (lhs, rhs) {
                case (.START, .END):
                    return true
                default:
                    return false
            }
        }
    }
}

// MARK: データセットを作る
func makeDataSet() -> [Task1]{
    var tasks = [Task1]()
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"
    let dateString = "2022-12-06"
    let date = dateFormatter.date(from: dateString)!

    // Data Set
    let startDate1 = date
    let endDate1 = Calendar.current.date(byAdding: .minute, value: 60, to: startDate1)!
    let startDate2 = Calendar.current.date(byAdding: .minute, value: 30, to: startDate1)!
    let endDate2 = Calendar.current.date(byAdding: .minute, value: 60, to: startDate1)!
    let startDate3 = Calendar.current.date(byAdding: .minute, value: 300, to: startDate1)!
    let endDate3 = Calendar.current.date(byAdding: .minute, value: 400, to: startDate1)!
    let startDate4 = Calendar.current.date(byAdding: .minute, value: 15, to: startDate1)!
    let endDate4 = Calendar.current.date(byAdding: .minute, value: 60, to: startDate1)!
    let startDate5 = Calendar.current.date(byAdding: .minute, value: 100, to: startDate1)!
    let endDate5 = Calendar.current.date(byAdding: .minute, value: 240, to: startDate1)!
    let startDate6 = Calendar.current.date(byAdding: .minute, value: 300, to: startDate1)!
    let endDate6 = Calendar.current.date(byAdding: .minute, value: 360, to: startDate1)!
    let startDate7 = Calendar.current.date(byAdding: .minute, value: 115, to: startDate1)!
    let endDate7 = Calendar.current.date(byAdding: .minute, value: 145, to: startDate1)!

    tasks.append(Task1(name: "Task 1", startDate: startDate1, endDate: endDate1, id: UUID()))
    tasks.append(Task1(name: "Task 2", startDate: startDate2, endDate: endDate2, id: UUID()))
    tasks.append(Task1(name: "Task 3", startDate: startDate3, endDate: endDate3, id: UUID()))
    tasks.append(Task1(name: "Task 4", startDate: startDate4, endDate: endDate4, id: UUID()))
    tasks.append(Task1(name: "Task 5", startDate: startDate5, endDate: endDate5, id: UUID()))
    tasks.append(Task1(name: "Task 6", startDate: startDate6, endDate: endDate6, id: UUID()))
    tasks.append(Task1(name: "Task 7", startDate: startDate7, endDate: endDate7, id: UUID()))

    return tasks
}
var tasks: [Task1] = makeDataSet()

// MARK: TaskをSTART, ENDの2つに分割する
func splitTasksIntoStartAndEndDates(tasks: [Task1]) -> [TimeMoment]{
    var timeMoments = [TimeMoment]()
    for (index, task) in tasks.enumerated() {
        timeMoments.append(TimeMoment(time: task.startDate, type: .START, index: index, id: task.id))
        timeMoments.append(TimeMoment(time: task.endDate, type: .END, index: index, id: task.id))
    }
    return timeMoments
}
var splitedTasks = splitTasksIntoStartAndEndDates(tasks: tasks)

// MARK: 3つのキーでソートする
let comparator = { (a: TimeMoment, b: TimeMoment) -> Bool in
    // もし1つめと2つめのtimeが異なるなら、値が小さい方を返す
    if a.time != b.time {
        return a.time < b.time
    }
    // もし1つめと2つめのtypeが異なるなら、STARTの方を返す
    if a.type != b.type {
        return a.type < b.type
    } else {
        return a.index < b.index
    }
}
splitedTasks.sort(by: comparator)

// OverlapしているRegionごとにArrayを作成する
var currentOverlap = Int.zero
var maxOverlap = Int.zero
var regions = [TimeMoment]()
// イベント毎にオーバーラップ数を格納する辞書を作成する
var taskOverlapCounts: [UUID: Int] = [:]

for event in splitedTasks {
    regions.append(event)
    // イベントの種類に応じてオーバーラップ数を増減
    if event.type == .START {
        currentOverlap += 1
    } else {
        currentOverlap -= 1
    }
    // 最大オーバーラップ数を更新
    maxOverlap = max(currentOverlap, maxOverlap)

    // オーバーラップ数が0になった場合は、dictに[task.id : maxOverlap]を追加していく
    if currentOverlap == 0 {
        for i in regions {
            taskOverlapCounts[i.id] = maxOverlap
        }
        regions.removeAll()
        maxOverlap = 0
    }
}
taskOverlapCounts

for i in tasks {
    print("\(i.name)は\(taskOverlapCounts[i.id]!)個のTaskと重なっています")
}
tasks

// MARK: CoredataからfetchしたTasksを回すときに...
// これはcoredataでtask.startDataでソートするのを再現するためのcomparator
let comparator2 = { (a: Task1, b: Task1) -> Bool in
        return a.startDate < b.startDate
}
// 各RegionのMaxOverlapを入れておく
var maxOverlapCountInCurrentRegion = 0
// Region内の何個目の要素にアクセス中かをカウントする。x軸に用いる。
var sequenceNumberInCurrentRegion = 0
for i in tasks.sorted(by: comparator2) {

    maxOverlapCountInCurrentRegion = taskOverlapCounts[i.id]!
    sequenceNumberInCurrentRegion += 1

    // 👉 ここでUIの処理

    let sampleString = """
    --------------------------------------------------
    \(i.name)は
    \(maxOverlapCountInCurrentRegion)個のTaakと重なっています。
    また、\(i.name)は、重なっているTaskのうち、
    \(sequenceNumberInCurrentRegion)個目のTaskです
    --------------------------------------------------
    """
    print(sampleString)

    // ⚠️ MARK: 以下の処理は、for loopの末尾に指定すること
    // 繰り返し処理をmaxOverlapCountInRegion回行ったら、両者を0にする
    if sequenceNumberInCurrentRegion == maxOverlapCountInCurrentRegion {
        maxOverlapCountInCurrentRegion; sequenceNumberInCurrentRegion = 0

    }    
}
tsonobe1 commented 1 year ago

↑ 解決じゃない

tsonobe1 commented 1 year ago

解決

//
//  TaskOverlapCountInRegion.swift
//  imonit
//
//  Created by 薗部拓人 on 2022/12/11.
//

import Foundation
import SwiftUI

// Task EntityをStartDateとEndDateに分割するための構造体
struct TimeMoment {
    var time: Date
    var type: TimeType
    var index: Int
    var id: UUID
    var overlap: Int = 0
//    var maxOverlap: Int = 0

    enum TimeType: Comparable {
        case START
        case END
        // 比較演算子を使用可能にする
        static func < (lhs: TimeType, rhs: TimeType) -> Bool {
            switch (lhs, rhs) {
            case (.START, .END):
                return true
            default:
                return false
            }
        }
    }
}

// MARK: TaskをSTART, ENDの2つに分割する
func splitTasksIntoStartAndEndDates(tasks: FetchedResults<Task>) -> [TimeMoment]{
    var timeMoments = [TimeMoment]()
    for (index, task) in tasks.enumerated() {
        timeMoments.append(TimeMoment(time: task.startDate!, type: .START, index: index, id: task.id!))
        timeMoments.append(TimeMoment(time: task.endDate!, type: .END, index: index, id: task.id!))
    }
    return timeMoments
}

// MARK: 3つのキーでソートする
let comparator = { (a: TimeMoment, b: TimeMoment) -> Bool in
    // もし1つめと2つめのtimeが異なるなら、値が小さい方を返す
    if a.time != b.time {
        return a.time < b.time
    }
    // もし1つめと2つめのtypeが異なるなら、STARTの方を返す
    if a.type != b.type {
        return a.type > b.type
    } else {
        return a.index < b.index
    }
}

// MARK: taskが他のTaskと重なっている数を、task.idに紐づけて返す
func getOverlapCount(splitedTasks: [TimeMoment]) -> [UUID: (maxOverlap: Int, xAxisOrder: Int)]{
    // イベント毎にオーバーラップ数を格納する辞書を作成する
    var taskOverlapCounts: [UUID: (maxOverlap: Int, xAxisOrder: Int)] = [:]
    var currentOverlap = 0
    var isHasBeenMinusCurrentOverlap = false
    var maxOverlap = 0
    var regions = [TimeMoment]()

    for event in splitedTasks {
        // イベントの種類に応じてオーバーラップ数を増減
        if event.type == .START {
            currentOverlap += 1
        } else {
            currentOverlap -= 1
//            isHasBeenMinusCurrentOverlap = true
        }
        // 最大オーバーラップ数を更新
        maxOverlap = max(currentOverlap, maxOverlap)

        if event.type == .START {
            regions.append(
                TimeMoment(
                    time: event.time,
                    type: event.type,
                    index: event.index,
                    id: event.id,
                    overlap: currentOverlap
                )
            )
        }

        // オーバーラップ数が0になった場合は、dictに[task.id : (maxOverlap, xOrder)]を追加していく
        if currentOverlap == 0 {
            let maxOverlapWhenStartType = maxOverlap

            for region in regions{
                    // taskのidをkeyとして、そのTaskのmaxOverlapと、x軸上の左から何番目に配置すべきかを引き出せるdict
                    taskOverlapCounts[region.id] = (
                        // 自身のTaskが所属するRegionのmaxOverlap
                        maxOverlap: maxOverlapWhenStartType,
                        // x軸上で、左から何番目に配置されるべきかを示したInt
                        xAxisOrder: region.overlap
                    )
                regions.removeAll()
                maxOverlap = 0
            }
        }
    }
    return taskOverlapCounts
}

// MARK: 上記の処理をまとめる
func getOverlapCountAndXAxisWithTaskID(tasks: FetchedResults<Task>) -> [UUID: (maxOverlap: Int, xAxisOrder: Int)] {
    var splitedTasks = splitTasksIntoStartAndEndDates(tasks: tasks)
    splitedTasks.sort(by: comparator)
    let overlapCountWithTaskID = getOverlapCount(splitedTasks: splitedTasks)
    return overlapCountWithTaskID
}

getOverlapCountAndXAxisWithTaskIDで得た辞書を、forで回したtask(CoreDataからfetchしたtaskEntity)で使うことで、そのtaskのmaxOverlap(widthの幅を、画面の横幅のなん分割にすべきか)と、xAxisOrder(x軸の左から何番目に配置すればよいか)を取得できる

tsonobe1 commented 1 year ago

↑は解決じゃない!

image

投稿では解決できていない問題

このようなコンテンツがある場合に、緑枠のTaskBoxが、2の位置に配置されてしまう。 そのため、別の方法を調べていたところ、以下の記事を見つけた。

https://stackoverflow.com/a/11323909/13646439

この回答の上から4つ目

スクリーンショット 2022-12-21 22 31 20

のjsbinのリンク先で、投稿では解決できていなかった問題を解決できていた。

以下スクショ

スクリーンショット 2022-12-21 22 34 52

このjavascriptをswiftで実装した。

次の投稿へ続く

tsonobe1 commented 1 year ago

javascript

$( document ).ready( function( ) {
  var column_index = 0;
  $( '#timesheet-events .daysheet-container' ).each( function() {

    var block_width = $(this).width();
    var columns = [];
    var lastEventEnding = null;

    // Create an array of all events
    var events = $('.bubble_selector', this).map(function(index, o) {
      o = $(o);
      var top = o.offset().top;
      return {
        'obj': o,
        'top': top,
        'bottom': top + o.height()
      };
    }).get();

    // Sort it by starting time, and then by ending time.
    events = events.sort(function(e1,e2) {
      if (e1.top < e2.top) return -1;
      if (e1.top > e2.top) return 1;
      if (e1.bottom < e2.bottom) return -1;
      if (e1.bottom > e2.bottom) return 1;
      return 0;
    });

    // Iterate over the sorted array
    $(events).each(function(index, e) {

//       console.log(e.top)
      if (lastEventEnding !== null && e.top >= lastEventEnding) {
        PackEvents( columns, block_width );
        columns = [];
        lastEventEnding = null;
      }

      var placed = false;
      for (var i = 0; i < columns.length; i++) {   

        var col = columns[ i ];
        if (!collidesWith( col[col.length-1], e ) ) {
          col.push(e);
          placed = true;
          break;
        }
      }

      if (!placed) {
        columns.push([e]);
      }

      if (lastEventEnding === null || e.bottom > lastEventEnding) {
        lastEventEnding = e.bottom;
      }
    });

    if (columns.length > 0) {
      PackEvents( columns, block_width );
    }
  });
});

function PackEvents( columns, block_width )
{
  var n = columns.length;
//         console.log("---------------")

  for (var i = 0; i < n; i++) {
    var col = columns[ i ];
    for (var j = 0; j < col.length; j++)

    {
      var bubble = col[j];
      console.log((i / n)*100)
      bubble.obj.css( 'left', (i / n)*100 + '%' );
      bubble.obj.css( 'width', block_width/n - 1 );
    }
  }
}

function collidesWith( a, b )
{
  return a.bottom > b.top && a.top < b.bottom;
}
   <body>
      <div id="timesheet-events">
        <!- ----------------------------------------------------------- -->
         <div class="daysheet-container" style="left: 0%;">
           <!- ----------------------------------------------------------- -->
            <div class="bubble-activator">
               <div class="bubble_selector" style="height: 28.662420382166px; left: 0px; top: 0px; width: 28px; ">
                  <div class="bubble-body">
                     <div style="border-color: #0000aa !important;" class="bubble-frame"></div>
                     <div style="background-color: #0000aa !important; border-color: #0000aa !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
            <div class="bubble-activator">
               <div class="bubble_selector" style="height: 40.108280254777px; top: 318.28571428571px; width: 28px; left: 28px; ">
                  <div class="bubble-body">
                     <div style="border-color: #0000aa !important;" class="bubble-frame"></div>
                     <div style="background-color: #0000aa !important; border-color: #0000aa !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
            <div class="bubble-activator">
               <div class="bubble_selector" style="height: 9.108280254777px; top: 400.7619047619px; width: 28px; left: 56px; ">
                  <div class="bubble-body">
                     <div style="border-color: #0000aa !important;" class="bubble-frame"></div>
                     <div style="background-color: #0000as !important; border-color: #0000aa !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
            <div class="bubble-activator">
               <div class="bubble_selector" style="height: 100.26751592357px; top: 20px; width: 28px; left: 84px; ">
                  <div class="bubble-body">
                     <div style="border-color: #992200 !important;" class="bubble-frame"></div>
                     <div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
           <div class="bubble-activator">
               <div class="bubble_selector" style="height: 50.26751592357px; top: 0px; width: 28px; left: 84px; ">
                  <div class="bubble-body">
                     <div style="border-color: #992200 !important;" class="bubble-frame"></div>
                     <div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
           <div class="bubble-activator">
               <div class="bubble_selector" style="height: 100.26751592357px; top: 300px; width: 28px; left: 84px; ">
                  <div class="bubble-body">
                     <div style="border-color: #992200 !important;" class="bubble-frame"></div>
                     <div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
           <div class="bubble-activator">
               <div class="bubble_selector" style="height: 100.26751592357px; top: 450px; width: 28px; left: 84px; ">
                  <div class="bubble-body">
                     <div style="border-color: #992200 !important;" class="bubble-frame"></div>
                     <div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
           <div class="bubble-activator">
               <div class="bubble_selector" style="height: 30.26751592357px; top: 415px; width: 28px; left: 84px; ">
                  <div class="bubble-body">
                     <div style="border-color: #992200 !important;" class="bubble-frame"></div>
                     <div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
           <!- ----------------------------------------------------------- -->
           <div class="bubble-activator">
               <div class="bubble_selector" style="height: 100.26751592357px; top: 450px; width: 28px; left: 84px; ">
                  <div class="bubble-body">
                     <div style="border-color: #992200 !important;" class="bubble-frame"></div>
                     <div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>
                      <!- ----------------------------------------------------------- -->
           <div class="bubble-activator">
               <div class="bubble_selector" style="height: 100.26751592357px; top: 29px; width: 28px; left: 84px; ">
                  <div class="bubble-body">
                     <div style="border-color: #992200 !important;" class="bubble-frame"></div>
                     <div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
                  </div>
               </div>
            </div>

以下変換したswift

import Foundation

var taskXPositionAndWidth: [UUID: (xPositionAsFractionOfLength: Double, widthRatio: Double)] = [:]

// MARK: Model
// CoreDataのEntityの再現
struct Task1 {
    var name: String
    var startDate: Date
    var endDate: Date
    var id: UUID
}

// MARK: データセットを作る
func makeDataSet() -> [Task1]{
    var tasks = [Task1]()
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"
    let dateString = "2022-12-06"
    let date = dateFormatter.date(from: dateString)!

    // Data Set
    let startDate1 = date
    let endDate1 = Calendar.current.date(byAdding: .minute, value: 50, to: startDate1)!
    let startDate2 = Calendar.current.date(byAdding: .minute, value: 0, to: startDate1)!
    let endDate2 = Calendar.current.date(byAdding: .minute, value: 100, to: startDate1)!
    let startDate3 = Calendar.current.date(byAdding: .minute, value: 30, to: startDate1)!
    let endDate3 = Calendar.current.date(byAdding: .minute, value: 150, to: startDate1)!
    let startDate4 = Calendar.current.date(byAdding: .minute, value: 300, to: startDate1)!
    let endDate4 = Calendar.current.date(byAdding: .minute, value: 550, to: startDate1)!
    let startDate5 = Calendar.current.date(byAdding: .minute, value: 350, to: startDate1)!
    let endDate5 = Calendar.current.date(byAdding: .minute, value: 400, to: startDate1)!
    let startDate6 = Calendar.current.date(byAdding: .minute, value: 600, to: startDate1)!
    let endDate6 = Calendar.current.date(byAdding: .minute, value: 700, to: startDate1)!
    let startDate7 = Calendar.current.date(byAdding: .minute, value: 800, to: startDate1)!
    let endDate7 = Calendar.current.date(byAdding: .minute, value: 900, to: startDate1)!
    let startDate8 = Calendar.current.date(byAdding: .minute, value: 1000, to: startDate1)!
    let endDate8 = Calendar.current.date(byAdding: .minute, value: 1500, to: startDate1)!
    let startDate9 = Calendar.current.date(byAdding: .minute, value: 1000, to: startDate1)!
    let endDate9 = Calendar.current.date(byAdding: .minute, value: 1500, to: startDate1)!
    let startDate10 = Calendar.current.date(byAdding: .minute, value: 50, to: startDate1)!
    let endDate10 = Calendar.current.date(byAdding: .minute, value: 150, to: startDate1)!

    tasks.append(Task1(name: "Task 1", startDate: startDate1, endDate: endDate1, id: UUID()))
    tasks.append(Task1(name: "Task 2", startDate: startDate2, endDate: endDate2, id: UUID()))
    tasks.append(Task1(name: "Task 3", startDate: startDate3, endDate: endDate3, id: UUID()))
    tasks.append(Task1(name: "Task 4", startDate: startDate4, endDate: endDate4, id: UUID()))
    tasks.append(Task1(name: "Task 5", startDate: startDate5, endDate: endDate5, id: UUID()))
    tasks.append(Task1(name: "Task 6", startDate: startDate6, endDate: endDate6, id: UUID()))
    tasks.append(Task1(name: "Task 7", startDate: startDate7, endDate: endDate7, id: UUID()))
    tasks.append(Task1(name: "Task 8", startDate: startDate8, endDate: endDate8, id: UUID()))
    tasks.append(Task1(name: "Task 9", startDate: startDate9, endDate: endDate9, id: UUID()))
    tasks.append(Task1(name: "Task 10", startDate: startDate10, endDate: endDate10, id: UUID()))

    return tasks
}
var tasks: [Task1] = makeDataSet()

// MARK: 開始時刻、終了時刻の順に並べ替える。
let comparator = { (e1: Task1, e2: Task1) -> Bool in
    if e1.startDate < e2.startDate { return true }
    if e1.startDate > e2.startDate { return false }
    if e1.endDate < e2.endDate { return true }
    if e1.endDate > e2.endDate { return false }
    return false
}

tasks.sort(by: comparator)

var columns: [[Task1]] = []
var lastEventEnding: Date?
var taskOverlapCounts: [UUID: (maxOverlap: Int, xAxisOrder: Int)] = [:]

tasks.enumerated().forEach { (index, task) in
    // A 今回のtaskが、前のtaskと時間が被っていない場合
    if lastEventEnding != nil && task.startDate >= lastEventEnding! {
       packEvents(columns: columns) // ⚠️
        columns = [] // columnsのリセット
        lastEventEnding = nil
    }

    // B カラムを順に見て、前のtaskと今のtaskが重なる場合、カラムに今のtaskを追加
    var placed = false
    for i in 0..<columns.count {

        let col = columns[i]
        // 前のtaskと今のtaskが重なっていない場合
        if !collidesWith( a: col[col.count - 1], b: task ){
            // ↓ colに代入をしていて手間取っていたが、swiftは参照渡しじゃないので、columns[i]にしないといけない。
            // ↓ javascriptの実装例では、colに代入していたが、jsはオブジェクトが参照渡しなので可能だっただけ。
            columns[i].append(task)
            placed = true
            break
        }
    }

    // C taskがどのカラムにも追加されなかった場合、新しいカラムを作成し、そこに今のtaskを追加
    if !placed {
        columns.append([task])
    }

    // D 今のtaskのendDateが前回のtaskのendDateより大きい場合、lastEventEndingを今のtaskのendDateに更新
    if lastEventEnding == nil || task.endDate > lastEventEnding! {
        lastEventEnding = task.endDate
    }
}
if !columns.isEmpty {
    packEvents(columns: columns)
}

// 2つのTaskの時間が重なっているかどうかを判定する, 被ってたらtrue
func collidesWith(a: Task1, b: Task1) -> Bool {
    a.endDate > b.startDate && a.startDate < b.endDate
}

func packEvents(columns: [[Task1]]) {
    let columnsCount = columns.count
    print("---------------------------")
    for (index, column) in columns.enumerated() {
        for task in column {
            let leftRate = Double(index) / Double(columnsCount)
            let widthRate = 100.0 / Double(columnsCount) / 100.0
            print("\(task.name), left \(leftRate), width \(widthRate)")
            taskXPositionAndWidth[task.id] = (xPositionAsFractionOfLength: leftRate, widthRatio: widthRate)
        }
    }
}

print("------------------------Dict----------------------------")
for i in taskXPositionAndWidth {
    print(i)
}

最終的に、taskXPositionAndWidthという、x軸上の左からの位置の比率と、x軸上の横幅の比率を、taskのidと紐付けた辞書を算出している。これを、SwiftUIのViewでtaskを描画する際に利用する。