16

I'm trying to achieve the simplest possible use case, but I can't figure it out. I have a picture of calendar. All I want is to show DatePicker popup when tapping the picture. I tried to put it inside ZStack, but by doing it I can't hide default data textfields:

ZStack {
    Image("icon-calendar")
    .zIndex(1)
    DatePicker("", selection: $date)
    .zIndex(2)
}

How to make this simple layout natively without ridiculous workarounds?

netsplatter
  • 559
  • 3
  • 14
  • 2
    It's impossible to open the `DatePicker` programmatically. Same for the underlying `UIDatePicker`: [Open UIDatePicker programmatically in iOS 14](https://stackoverflow.com/q/63331669/8697793) – pawello2222 Jan 19 '21 at 23:19
  • 1
    Thanks for the new API, Apple. Great job. – teradyl Jun 02 '21 at 16:35
  • You can do it using the accessibility API. https://stackoverflow.com/q/75073023/77567 – rob mayoff Jan 10 '23 at 19:15

6 Answers6

15

I have googled hundred times and finally, I found a way to achieve this. It's 1:50 AM in my timezone, I can sleep happily now. Credit goes to chase's answer here

Demo here: https://media.giphy.com/media/2ILs7PZbdriaTsxU0s/giphy.gif

The code that does the magic

struct ContentView: View {
    @State var date = Date()
    
    var body: some View {
        ZStack {
            DatePicker("label", selection: $date, displayedComponents: [.date])
                .datePickerStyle(CompactDatePickerStyle())
                .labelsHidden()
            Image(systemName: "calendar")
                .resizable()
                .frame(width: 32, height: 32, alignment: .center)
                .userInteractionDisabled()
        }
    }
}

struct NoHitTesting: ViewModifier {
    func body(content: Content) -> some View {
        SwiftUIWrapper { content }.allowsHitTesting(false)
    }
}

extension View {
    func userInteractionDisabled() -> some View {
        self.modifier(NoHitTesting())
    }
}

struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
    let content: () -> T
    func makeUIViewController(context: Context) -> UIHostingController<T> {
        UIHostingController(rootView: content())
    }
    func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
General Grievance
  • 4,555
  • 31
  • 31
  • 45
Hieu Dinh
  • 692
  • 5
  • 18
10

For those still looking for a simple solution, I was looking for something similar and found a great example of how to do this in one of Kavasoft's tutorials on YouTube at 20:32 into the video.

This is what he used:

import SwiftUI

struct DatePickerView: View {

    @State private var birthday = Date()
    @State private var isChild = false
    @State private var ageFilter = ""

    var body: some View {

        Image(systemName: "calendar")
          .font(.title3)
          .overlay{ //MARK: Place the DatePicker in the overlay extension
             DatePicker(
                 "",
                 selection: $birthday,
                 displayedComponents: [.date]
             )
              .blendMode(.destinationOver) //MARK: use this extension to keep the clickable functionality
              .onChange(of: birthday, perform: { value in
                  isChild = checkAge(date:birthday)
               })
          }
    }

    //MARK: I added this function to show onChange functionality remains the same

    func checkAge(date: Date) -> Bool  {
        let today = Date()
        let diffs = Calendar.current.dateComponents([.year], from: date, to: today)
        let formatter = DateComponentsFormatter()
        let outputString = formatter.string(from: diffs)
        self.ageFilter = outputString!.filter("0123456789.".contains)
        let ageTest = Int(self.ageFilter) ?? 0
        if ageTest > 18 {
            return false
        }else{
            return true
        }
    }
}

    

 

The key is put the DatePicker in an overlay under the Image. Once done, the .blendmode extension needs to be set to .desintationOver for it to be clickable. I added a simple check age function to show onChange functionality remains the same when using it in this way.

I tested this code in Xcode 14 (SwiftUI 4.0 and IOS 16).

I hope this helps others!

Demo

Demo Image DatePicker

Cslim
  • 303
  • 2
  • 8
  • This is REALLY a GREAT solution. Seems to work perfectly when used in regular views where the "Calendar" icon needs to trigger a date change..! Best & simplest solution from my point of view. My hat off to you @Cslim! – Gerard Feb 04 '23 at 17:23
  • This works from iOS 15+ – Vodenjak Jul 13 '23 at 12:55
6

Tried using Hieu's solution in a navigation bar item but it was breaking. Modified it by directly using SwiftUIWrapper and allowsHitTesting on the component I want to display and it works like a charm.

Also works on List and Form

struct StealthDatePicker: View {
    @State private var date = Date()
    var body: some View {
        ZStack {
            DatePicker("", selection: $date, in: ...Date(), displayedComponents: .date)
                .datePickerStyle(.compact)
                .labelsHidden()
            SwiftUIWrapper {
                Image(systemName: "calendar")
                .resizable()
                .frame(width: 32, height: 32, alignment: .topLeading)
            }.allowsHitTesting(false)
        }
    }
}

struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
    let content: () -> T
    func makeUIViewController(context: Context) -> UIHostingController<T> {
        UIHostingController(rootView: content())
    }
    func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Sharath Sriram
  • 160
  • 2
  • 5
4

My answer to this was much simpler... just create a button with a popover that calls this struct I created...

struct DatePopover: View {

@Binding var dateIn: Date
@Binding var isShowing: Bool

    var body: some View {
        VStack {
            DatePicker("", selection: $dateIn, displayedComponents: [.date])
                .datePickerStyle(.graphical)
                .onChange(of: dateIn, perform: { value in
                 
                    isShowing.toggle()
                })
            .padding(.all, 20)
        }.frame(width: 400, height: 400, alignment: .center)
    }
    
}

Not sure why, but it didn't format my code like I wanted...

( Original asnwer had button, onChange is better solution)

Sample of my Button that calls it... it has my vars in it and may not make complete sense to you, but it should give you the idea and use in the popover...

    Button(item.dueDate == nil ? "" : dateValue(item.dueDate!)) {
        if item.dueDate != nil { isUpdatingDate = true }
        }
        .onAppear { tmpDueDate = item.dueDate ?? .now }
        .onChange(of: isUpdatingDate, perform: { value in
                if !value {
                        item.dueDate = tmpDueDate
                        try? moc.save()
                }
        })
        .popover(isPresented: $isUpdatingDate) {
           DatePopover(dateIn: $tmpDueDate, isShowing: $isUpdatingDate)
        }

FYI, dateValue() is a local func I created - it simply creates a string representation of the Date in my format

kelalaka
  • 5,064
  • 5
  • 27
  • 44
tj4shee
  • 371
  • 1
  • 2
  • 11
  • This is a nice solution that even works on different Z-levels. Also, the button is not required. I've updated the answer. – kelalaka Oct 14 '22 at 17:39
2
struct ZCalendar: View {
    @State var date = Date()
    @State var isPickerVisible = false
    var body: some View {
        ZStack {
            Button(action: {
                isPickerVisible = true
            }, label: {
                Image(systemName: "calendar")
            }).zIndex(1)
            if isPickerVisible{
                VStack{
                    Button("Done", action: {
                        isPickerVisible = false
                    }).padding()
                    DatePicker("", selection: $date).datePickerStyle(GraphicalDatePickerStyle())
                }.background(Color(UIColor.secondarySystemBackground))
                .zIndex(2)
            }
        }//Another way
        //.sheet(isPresented: $isPickerVisible, content: {DatePicker("", selection: $date).datePickerStyle(GraphicalDatePickerStyle())})
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Thank you for your suggestion, but this code is just showing VStack container on button tap, not a calendar popup action. In case of .sheet it's the same wrapping of DatePicker, but not initiating the picker popup itself. – netsplatter Jan 19 '21 at 21:48
  • What do you mean by initiating? are you talking about the Graphical Date Picker? That comes after what you wrote? That is just a style – lorem ipsum Jan 19 '21 at 21:52
  • 1
    Using your example first I tap on icon, then I tap on default DatePicker's textField to get that calendar popup where you actually choose date. What I need is to be able to just tap custom icon and get the popup without having to deal with DatePicker's textFields. – netsplatter Jan 19 '21 at 22:13
  • Look at the new code. If all you want is the Calendar looking view you just change the style to the Graphical – lorem ipsum Jan 19 '21 at 22:20
  • Then I'm afraid I will have to create this custom popup window using .sheet workaround. So there's no native way of doing what I need? And yes, I'm aware of all the styling, that's clear and fun. But still not what I need to keep it all simple. – netsplatter Jan 19 '21 at 22:26
  • 1
    Nope, that is as simple as it gets. – lorem ipsum Jan 19 '21 at 22:36
1

Please understand that my sentence is weird because I am not good at English.

In the code above, if you use .frame() & .clipped().
Clicks can be controlled exactly by the icon size.

In the code above, I modified it really a little bit. I found the answer. Thank you.

import SwiftUI

struct DatePickerView: View {
    @State private var date = Date()
    
    var body: some View {
        
        ZStack{
            DatePicker("", selection: $date, displayedComponents: .date)
                .labelsHidden()
                .datePickerStyle(.compact)
                .frame(width: 20, height: 20)
                .clipped()
 
            SwiftUIWrapper {
                Image(systemName: "calendar")
                    .resizable()
                    .frame(width: 20, height: 20, alignment: .topLeading)
            }.allowsHitTesting(false)
        }//ZStack
    }
}

struct DatePickerView_Previews: PreviewProvider {
    static var previews: some View {
        DatePickerView()
    }
}

struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
    let content: () -> T
    func makeUIViewController(context: Context) -> UIHostingController<T> {
        UIHostingController(rootView: content())
    }
    func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
Tyler2P
  • 2,324
  • 26
  • 22
  • 31
Ruyha
  • 11
  • 2