35

I'm implementing a calendar view, and I'd like it to start at the beginning of the week containing a particular date. Eg. If the target date is Monday, Feb 29, 2016, and the current calendar is set to start on Sunday, I'd like my view to start with Sunday, February 28.

This seems like it should be straightforward:

let calendar = NSCalendar.currentCalendar()
let firstDate = calendar.nextDateAfterDate(targetDate, 
                    matchingUnit: .Weekday,
                           value: calendar.firstWeekday,
                         options: .SearchBackwards)

But this fails with:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Exactly one option from the set {NSCalendarMatchPreviousTimePreservingSmallerUnits, NSCalendarMatchNextTimePreservingSmallerUnits, NSCalendarMatchNextTime} must be specified.'

I can get basically what I want with:

let firstDate = calendar.nextDateAfterDate(firstDate, 
                    matchingUnit: .Weekday,
                           value: calendar.firstWeekday,
                        options: .MatchPreviousTimePreservingSmallerUnits)?
                    .dateByAddingTimeInterval(-7 * 84600)

But it seems like a bad practice, since sometimes the number of seconds in a day isn't 86400.

Is there a better way?

Aneel
  • 1,403
  • 1
  • 17
  • 22

7 Answers7

93

you can use Calendar method date(from: DateComponents) passing [.yearForWeekOfYear, .weekOfYear] components from any date it will return the first day of the week from the calendar used. So if you would like to get Sunday just use Gregorian calendar. If you would like to get the Monday as the first day of the week you can use Calendar .iso8601 as you can see in this answer

Xcode 12 • Swift 5.3 or later (works with previous Swift versions as well)

extension Calendar {
    static let gregorian = Calendar(identifier: .gregorian)
}

extension Date {
    func startOfWeek(using calendar: Calendar = .gregorian) -> Date {
        calendar.dateComponents([.calendar, .yearForWeekOfYear, .weekOfYear], from: self).date!
    }
}

usage:

Date().startOfWeek()  // "Sep 20, 2020 at 12:00 AM"


If you would like to get the beginning of week at a particular timezone you just need to use a custom calendar:

var gregorianUTC = Calendar.gregorian
gregorianUTC.timeZone = TimeZone(identifier: "UTC")!
print(Date().startOfWeek(using: gregorianUTC))  // "2020-09-20 00:00:00 +0000\n"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • 4
    Thanks! This is an elegant way to do it. – Aneel Feb 28 '16 at 22:51
  • This is cool way to achieve it, but it has a minor issue with daylight saving time. This can be fixed with the following code: `var startOfWeek: NSDate { let date = Calendar.gregorian.dateFromComponents(Calendar.gregorian.components([.YearForWeekOfYear, .WeekOfYear ], fromDate: self))!; let dslTimeOffset = NSTimeZone.local.daylightSavingTimeOffset(for: date); return date.addingTimeInterval(dslTimeOffset); }` – elquimista Nov 08 '16 at 01:30
  • 4
    instead of gregorian it's better to use NSCalendar.autoupdatingCurrent – Adam Smaka Feb 16 '17 at 12:01
  • 1
    You have to use Calendar.current instead of Calendar(identifier: .gregorian) – Olav Gausaker Feb 27 '17 at 16:33
  • No I don't. I want to find out Sunday date and Gregorian calendar will always work for that purpose. BTW I am not sure but I think the only calendar that Sunday it is not the first day in the week is iso8601 calendar – Leo Dabus Feb 27 '17 at 16:37
  • Is there ever a reason that this would return nil? – Matthew Knippen May 10 '17 at 14:38
  • 1
    Probably not but I prefer to keep it optional and use guard as usual – Leo Dabus May 10 '17 at 14:45
  • Is it possible to choose gregorian/.iso8601 automatically? – Vyachaslav Gerchicov Aug 04 '20 at 06:23
  • @VyachaslavGerchicov I don't know what you mean by "choose gregorian/.iso8601 automatically" You can use a fixed calendar or use the current one. – Leo Dabus Aug 04 '20 at 07:29
  • @LeoDabus Your `startOfWeek` doesn't work for `Calendar(identifier: .chinese)`. I believe it has something to do with `.era` since the date returned will be 1984, but even passing `.era` component has no effect and will result in a date with the year 1984. `calendar.dateInterval(of: .weekOfYear, for: Date())` seems to work for all calendars. – hidden-username Oct 13 '21 at 17:45
  • I'm curious to figure out endOfWeek. – Sylar Feb 09 '22 at 07:37
6

Swift 4 Solution

I have figured out according to my requirement, where I have find out dates for following.

1. Today

2. Tomorrow 

3. This Week 

4. This Weekend 

5. Next Week 

6. Next Weekend

So, I have created Date Extension to get Dates of Current Week and Next Week.

CODE

extension Date {

    func getWeekDates() -> (thisWeek:[Date],nextWeek:[Date]) {
        var tuple: (thisWeek:[Date],nextWeek:[Date])
        var arrThisWeek: [Date] = []
        for i in 0..<7 {
            arrThisWeek.append(Calendar.current.date(byAdding: .day, value: i, to: startOfWeek)!)
        }
        var arrNextWeek: [Date] = []
        for i in 1...7 {
            arrNextWeek.append(Calendar.current.date(byAdding: .day, value: i, to: arrThisWeek.last!)!)
        }
        tuple = (thisWeek: arrThisWeek,nextWeek: arrNextWeek)
        return tuple
    }

    var tomorrow: Date {
        return Calendar.current.date(byAdding: .day, value: 1, to: noon)!
    }
    var noon: Date {
        return Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)!
    }

    var startOfWeek: Date {
        let gregorian = Calendar(identifier: .gregorian)
        let sunday = gregorian.date(from: gregorian.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self))
        return gregorian.date(byAdding: .day, value: 1, to: sunday!)!
    }

    func toDate(format: String) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = format
        return formatter.string(from: self)
    }
}

USAGE:

let arrWeekDates = Date().getWeekDates() // Get dates of Current and Next week.
let dateFormat = "MMM dd" // Date format
let thisMon = arrWeekDates.thisWeek.first!.toDate(format: dateFormat)
let thisSat = arrWeekDates.thisWeek[arrWeekDates.thisWeek.count - 2].toDate(format: dateFormat)
let thisSun = arrWeekDates.thisWeek[arrWeekDates.thisWeek.count - 1].toDate(format: dateFormat)

let nextMon = arrWeekDates.nextWeek.first!.toDate(format: dateFormat)
let nextSat = arrWeekDates.nextWeek[arrWeekDates.nextWeek.count - 2].toDate(format: dateFormat)
let nextSun = arrWeekDates.nextWeek[arrWeekDates.nextWeek.count - 1].toDate(format: dateFormat)

print("Today: \(Date().toDate(format: dateFormat))") // Sep 26
print("Tomorrow: \(Date().tomorrow.toDate(format: dateFormat))") // Sep 27
print("This Week: \(thisMon) - \(thisSun)") // Sep 24 - Sep 30
print("This Weekend: \(thisSat) - \(thisSun)") // Sep 29 - Sep 30
print("Next Week: \(nextMon) - \(nextSun)") // Oct 01 - Oct 07
print("Next Weekend: \(nextSat) - \(nextSun)") // Oct 06 - Oct 07

You can modify Extension according to your need.

Thanks!

Rashesh Bosamiya
  • 609
  • 1
  • 9
  • 13
5

You can implement this as Date class extension or something. It should returns something like 2020-01-06 00:00:00 +0000

Xcode 11.3 Swift 5

func firstDayOfWeek() -> Date {
    var c = Calendar(identifier: .iso8601)
    c.timeZone = TimeZone(secondsFromGMT: 0)!
    print(
        c.date(from: c.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date()))!
    )
} 
kpostekk
  • 522
  • 5
  • 6
3

The Calendar has a mechanism for finding date at the start of a given time interval (say week of year, or month) that contains a given date:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let date = dateFormatter.date(from: "2017-01-07")

if let date = date {
    let calendar = Calendar(identifier: .gregorian)

    var startDate : Date = Date()
    var interval : TimeInterval = 0

    if calendar.dateInterval(of: .weekOfYear, start: &startDate, interval: &interval, for: date) {
        print("Start of week is \(startDate)")
        // prints "Start of week is 2017-01-01 06:00:00 +0000"
    }
}
Scott Thompson
  • 22,629
  • 4
  • 32
  • 34
0

In order to get the user's locale settings respected correctly, you should use the user's Calendar firstWeekday property in the DateComponents. This is what I usually use:

// MARK: first day of week
extension Date {
  /**
    Finds the first day of the week the subject date falls into.
   
   - Parameter calendar: The calendar to use. Defaults to the user's current calendar.
   
   - Returns: The `Date` of the first day of the week into which the subject date falls.
   
   `startOfWeek()` respects the user's locale settings, i.e. will automatically use Sunday/Monday/etc. as first
   weekday based on the user's region and locale settings.
   */
  func startOfWeek(using calendar: Calendar = .current) -> Date? {
    var components = calendar.dateComponents([.weekday, .year, .month, .weekOfYear], from: self)
    components.weekday = calendar.firstWeekday
    return calendar.date(from: components)
  }
}

Haensl
  • 343
  • 3
  • 16
-1

Basically use

NSCalender

and

dateByAddingComponents

. For solving of you're problem try to use this code sample:

let cal = NSCalendar.currentCalendar()

let components = NSDateComponents()
components.weekOfYear -= 1

if let date = cal.dateByAddingComponents(components, toDate: NSDate(), options: NSCalendarOptions(0)) {
    var beginningOfWeek: NSDate?
    var weekDuration = NSTimeInterval()
    if cal.rangeOfUnit(.CalendarUnitWeekOfYear, startDate: &beginningOfWeek, interval: &weekDuration, forDate: date) {
         print(beginningOfWeek) 
    }
}
Oleg Gordiichuk
  • 15,240
  • 7
  • 60
  • 100
  • Thanks! Though I ended up going with another way of solving the problem, this answer helped me understand better how to use dateByAddingComponents. – Aneel Feb 28 '16 at 22:54
-1

I had problems with all previous solutions, since they do not take into account user's calendar setting. Next code will be taking into account that.

extension Date {    

    var startOfWeek: Date? {
        let calendar = Calendar.current
        var components: DateComponents? = calendar.dateComponents([.weekday, .year, .month, .day], from: self)
        var modifiedComponent = components
        modifiedComponent?.day = (components?.day ?? 0) - ((components?.weekday ?? 0) - 1)

        return calendar.date(from: modifiedComponent!)
    }

    var endOfWeek: Date? {
        let calendar = Calendar.current
        var components: DateComponents? = calendar.dateComponents([.weekday, .year, .month, .day], from: self)
        var modifiedComponent = components
        modifiedComponent?.day = (components?.day ?? 0) + (7 - (components?.weekday ?? 0))
        modifiedComponent?.hour = 23
        modifiedComponent?.minute = 59
        modifiedComponent?.second = 59

        return calendar.date(from: modifiedComponent!)

    }
}
lesyk
  • 3,979
  • 3
  • 25
  • 39