skip to Main Content

I’m trying to make a scheme in which I have 7 fixed days above, the current day is always the middle one, but I’d like to make the other days clickable too, so that when you click on the day the card below changes with the corresponding descriptions, as if each one were linked to a specific day. I managed to set the days, but I can’t figure out if I need to use a state or something similar to make them clickable.

At first I tried using state and manipulating it in zstack, but it didn’t work.

import SwiftUI

struct DayPlanView: View {
    @State var monthString: String = "Not Set"

    let calendar = Calendar.current
     

    var body: some View {
        let dates = getWeek()
        
        VStack {
            // MARK: Change Date() to some identifier of selected date.
            HStack {
                Text("(getDayNumber(date: Date()))")
                    .font(.title2)
                    .fontWeight(.semibold)
                Text("of")
                    .font(.title2)
                    .fontWeight(.semibold)
                Text(getMonth(date: Date()))
                    .font(.title2)
                    .fontWeight(.semibold)
            }
            
            HStack {
                ForEach(dates, id: .self) { day in
                    VStack {


                        let dayNumber = getDayNumber(date: day)
                        
                        if dayNumber == getDayNumber(date: Date()) {
                            ZStack {
                                Circle()
                                    .fill(.blue)
                                    .scaleEffect(1.3)
                                
                                Text("(getDayNumber(date: day))")
                                    .font(.title)
                                    .fontWeight(.semibold)
                                    .foregroundColor(.white)
                            }
                        } else {
                            Text("(getDayNumber(date: day))")
                               .font(.title)
                        }
                    }
                    .frame(width: getWidth() / 8, height: getHeight() / 20)
                    .padding(.horizontal, -3)
               }
            }
           .frame(width: getWidth())
            Spacer()
        }
        .padding(.vertical, getHeight() / 20)
    }
    
    func getMonth(date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "LLLL"
        return dateFormatter.string(from: date)
    }

    func getDayShort(date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E"
        return dateFormatter.string(from: date)
    }
    
    func getDayNumber(date: Date) -> Int {
        let calendar = Calendar.current
        let components = calendar.dateComponents([.day], from: date)
        return components.day ?? 0
    }

    func getWeek() -> [Date] {
        let currentDate = Date()

        let calendar = Calendar.current
        let dayOfWeek = calendar.component(.weekday, from: currentDate)
        
        let range = calendar.range(of: .day, in: .month, for: currentDate)!
        
        let daysMonth = (range.lowerBound - 1 ..< range.lowerBound + 6)
            .compactMap { calendar.date(byAdding: .day, value: $0 - dayOfWeek, to: currentDate) }
        
        return daysMonth
    }
}

struct DayPlanView_Previews: PreviewProvider {
    static var previews: some View {
        DayPlanView()
    }
}

the screen I'm working

2

Answers


  1. I would go with a binding. From copying your code and modifying it you can check the following.

    It is not perfect but it should get you on the right track.

    struct ContentView: View {
        
        @State private var selectedDate: Date = Date()
        
        var body: some View {
            VStack {
                DayPlanView(selectedDate: $selectedDate)
                Text("Hello, world!")
            }
            .padding()
        }
    }
    
    #Preview {
        ContentView()
    }
    
    struct DayPlanView: View {
        @State var monthString: String = "Not Set"
    
        let calendar = Calendar.current
        
        @Binding var selectedDate: Date
         
    
        var body: some View {
            let dates = getWeek()
            
            GeometryReader { proxy in
                VStack {
                    // MARK: Change Date() to some identifier of selected date.
                    HStack {
                        Text("(getDayNumber(date: Date()))")
                            .font(.title2)
                            .fontWeight(.semibold)
                        Text("of")
                            .font(.title2)
                            .fontWeight(.semibold)
                        Text(getMonth(date: Date()))
                            .font(.title2)
                            .fontWeight(.semibold)
                    }
                    
                    HStack {
                        ForEach(dates, id: .self) { day in
                            VStack {
    
    
                                let dayNumber = getDayNumber(date: day)
                                
                                if dayNumber == getDayNumber(date: selectedDate) {
                                    ZStack {
                                        Circle()
                                            .fill(.blue)
                                            .scaleEffect(1.3)
                                        
                                        Text("(getDayNumber(date: day))")
                                            .font(.title)
                                            .fontWeight(.semibold)
                                            .foregroundColor(.white)
                                    }
                                } else {
                                    Text("(getDayNumber(date: day))")
                                       .font(.title)
                                }
                            }
                            .frame(width: proxy.size.width / 8, height: proxy.size.height / 20)
                            .padding(.horizontal, -3)
                            .onTapGesture {
                                selectedDate = day
                            }
                       }
                    }
                   .frame(width: proxy.size.width)
                    Spacer()
                }
                .padding(.vertical, proxy.size.height / 20)
            }
            
        }
        
        func getMonth(date: Date) -> String {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "LLLL"
            return dateFormatter.string(from: date)
        }
    
        func getDayShort(date: Date) -> String {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "E"
            return dateFormatter.string(from: date)
        }
        
        func getDayNumber(date: Date) -> Int {
            let calendar = Calendar.current
            let components = calendar.dateComponents([.day], from: date)
            return components.day ?? 0
        }
    
        func getWeek() -> [Date] {
            let currentDate = selectedDate
    
            let calendar = Calendar.current
            let dayOfWeek = calendar.component(.weekday, from: currentDate)
            
            let range = calendar.range(of: .day, in: .month, for: currentDate)!
            
            let daysMonth = (range.lowerBound - 1 ..< range.lowerBound + 6)
                .compactMap { calendar.date(byAdding: .day, value: $0 - dayOfWeek, to: currentDate) }
            
            return daysMonth
        }
    }
    
    struct DayPlanView_Previews: PreviewProvider {
        static var previews: some View {
            DayPlanView(selectedDate: .constant(Date()))
        }
    }
    
    Login or Signup to reply.
  2. Yes, you can use an @State in your top view which is passed to a @Binding in your day selector view.

    The first thing I did was create a day selector view, which accepts an array of Dates to show and a selectedDate binding –

    struct DateSelectorView: View {
        static var calendar = Calendar.autoupdatingCurrent
        var dates:[Date] = []
        @Binding var selectedDate: Date
        
        var body: some View {
            VStack(spacing: 8) {
                HStack {
                    Spacer()
                    Text("(selectedDate, format: Date.FormatStyle(date: .complete, time: .omitted))").font(.title2).fontWeight(.semibold)
                    Spacer()
                }
                HStack {
                    ForEach(self.dates, id:.self) { date in
                        DateCircleView(date: date, selected: DateSelectorView.calendar.isDate(date, equalTo: selectedDate, toGranularity: .day)).onTapGesture {
                            self.selectedDate = date
                        }
                    }
                }
            }
        }
    }
    
    struct DateCircleView: View {
        var date: Date
        var selected: Bool
        
        var body: some View {
            ZStack {
                if selected {
                    Circle().scaleEffect(1.3).foregroundColor(.blue)
                }
                Text("(dayNumber)")
                    .foregroundColor(selected ? .white: .black)
                    .font(.title)
            }.fixedSize(horizontal: true, vertical: true)
        }
        
        var dayNumber: Int {
            return DateSelectorView.calendar.component(.day, from: date)
        }
    }
    
    #Preview {
        var dates = [Date]()
        for i in -3...3 {
            dates.append(DateSelectorView.calendar.date(byAdding: .day, value: i, to: Date())!)
        }
        return DateSelectorView(dates:dates, selectedDate:.constant(Date()))
    }
    

    An onTapGesture is used to assign the selected date to the @Binding property

    I created a simple top-level view and DayContentView to show how to use it:

    struct ContentView: View {
        @State var selectedDate = Date()
        var body: some View {
            VStack {
                DateSelectorView(dates: dates(for: Date()), selectedDate: $selectedDate)
                DayContentView(selectedDate: $selectedDate)
                Spacer()
            }
            .padding()
        }
        
        func dates(for startDate: Date) -> [Date] {
            var dates = [Date]()
            for i in -3...3 {
                dates.append(DateSelectorView.calendar.date(byAdding: .day, value: i, to: startDate)!)
            }
            return dates
        }
    }
    
    struct DayContentView: View {
        @Binding var selectedDate: Date
        var body: some View {
            ZStack {
                
                RoundedRectangle(cornerRadius:25.0)
                    .fill(.blue)
                    .frame(height:120)
                
                VStack {
                    HStack {
                        Text("You have selected:")
                        Spacer()
                    }
                    Text("(selectedDate, format:  Date.FormatStyle(date: .complete, time: .omitted))")
                }
                .foregroundColor(.white)
                .padding()
                
            }
        }
    }
    

    I also used a handy extension on Date (from this answer to find the first day of the week for a given Date:

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

    With a bit of a tweak you can use a matchedGeometryEffect to get a nice animation of the blue selection circle:

    struct DateSelectorView: View {
        static var calendar = Calendar.autoupdatingCurrent
        var dates:[Date] = []
        @Binding var selectedDate: Date
        @Namespace var namespace
        
        var body: some View {
            VStack(spacing: 8) {
                HStack {
                    Spacer()
                    Text("(selectedDate, format: Date.FormatStyle(date: .complete, time: .omitted))").font(.title2).fontWeight(.semibold)
                    Spacer()
                }
                HStack {
                    ForEach(self.dates, id:.self) { date in
                        DateCircleView(date: date, selected: DateSelectorView.calendar.isDate(date, equalTo: selectedDate, toGranularity: .day), namespace: namespace).onTapGesture {
                            withAnimation(.bouncy) {
                                self.selectedDate = date
                            }
                        }
                    }
                }
            }
        }
    }
    
    struct DateCircleView: View {
        var date: Date
        var selected: Bool
        var namespace: Namespace.ID
        
        var body: some View {
            ZStack {
                if selected {
                    Circle().scaleEffect(1.3).foregroundColor(.blue).matchedGeometryEffect(id:  "selected", in: namespace)
                }
                Text("(dayNumber)")
                    .foregroundColor(selected ? .white: .black)
                    .font(.title)
            }.fixedSize(horizontal: true, vertical: true)
        }
        
        var dayNumber: Int {
            return DateSelectorView.calendar.component(.day, from: date)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search