How to give DatePicker a minute interval in SwiftUI - swift

does anyone know how to give the minutes DatePicker an interval? I am trying to only display multiples of 15 (0, 15, 30, 45). Is this only yet possible by interfacing with UIKit?
I believe it's not using the in: ...Date() part, at least I couldn't think of a solution. Here is my snippet:
DatePicker(
"",
selection: $selectedDate,
/*in: ...Date(),*/
displayedComponents: .hourAndMinute
)
Thanks so much!

The solution is to configure using UIDatePicker.appearance before SwiftUI DatePicker is created.
Tested with Xcode 13.3 / iOS 15.4
Main part is:
UIDatePicker.appearance().minuteInterval = 15
Complete code in here

can be tried this way:
struct DatePickerView: View {
#Binding var dateSelected: Date
var type: DatePickerComponents? = .date
var body: some View {
DatePicker("", selection: $dateSelected, displayedComponents: .hourAndMinute)
.datePickerStyle(.wheel)
.onAppear {
if type == .hourAndMinute {
UIDatePicker.appearance().minuteInterval = 15
} else {
UIDatePicker.appearance().minimumDate = dateSelected
}
}
}
}

What about using two pickers, one for hour and one for minute, and then converting to Date() afterwards? It's not pretty but this gets part of the way there:
struct ContentView: View {
#State private var hour = 1
#State private var minute = 0
let minutes = [0, 15, 30, 45]
var body: some View {
GeometryReader { geometry in
HStack {
Picker("Hour", selection: self.$hour) {
ForEach(1...12, id: \.self) {
Text("\($0)")
}
}
.labelIsHidden()
.frame(maxWidth: geometry.size.width / 4)
.clipped()
Picker("Minutes", selection: self.$minute) {
ForEach(self.minutes, id: \.self) { minute in
Text(minute == 0 ? " : 0\(minute)" : " : \(minute)")
}
}
.labelIsHidden()
.frame(maxWidth: geometry.size.width / 4)
.clipped()
}
}
}
}

Related

Contracting an expanded DatePicker on SwiftUI / MacOS

Is there a way to get a DatePicker that has been expanded to contract when the user clicks on a date?
The following code will crash the app - or at least put it into the locked state.
There is a Text component showing the date. When you click on it the DatePicker appears in its place - but there is no way to dismiss it after it is an expanded mode. Trying to swap back to the original Text, by setting edit mode to false when the date changes effectively locks the app:
struct ContentView: View {
#State private var date: Date = .now
#State private var editMode: Bool = false
var body: some View {
VStack {
if editMode {
DatePicker("Test", selection: $date)
.labelsHidden()
.onChange(of: date) { _ in
editMode = false
}
.frame(width: 50)
} else {
Text(date.formatted(date: .abbreviated, time: .omitted))
.onTapGesture {
editMode = true
}
}
}
.padding()
}
}
Ideally I'd also be able to dismiss it programatically.
I have tried using a ZStack with opacity control, such as:
struct ContentView: View {
#State private var date: Date = .now
#State private var editMode: Bool = false
var body: some View {
ZStack {
DatePicker("Test", selection: $date)
.labelsHidden()
.onChange(of: date) { _ in
editMode = false
}
.frame(width: 50)
.opacity(editMode ? 1.0 : 0.0)
Text(date.formatted(date: .abbreviated, time: .omitted))
.onTapGesture {
editMode = true
}
.opacity(editMode ? 0.0 : 1.0)
}
.padding()
}
}
But this isn't working in my actual use case. I'd really like to know if there was a way of dismissing that DatePicker expanded form.

Is there a way to get my Apple Watch App to vibrate when the screen sleeps?

I own a Apple Watch series 3 and I made an app for it that vibrates every specified amount of seconds. I know haptics drain your battery but, regardless, its the purpose of my app. I tried to test it on my watch and I encountered a problem. When the screen is on the app functions like its supposed to but as soon as I put my wrist down and the screen sleeps, the app runs in the background but it doesn't vibrate like supposed to. Is there a way around this? Like I said I have a series 3 therefore I don't have access to the always on screen display function.
My app code is below:
import SwiftUI
struct ContentView: View {
#State var timerScreenShown = false
#State var timeVal = 10
var body: some View {
VStack{
Text("Select \(timeVal)s intervals").padding()
Picker(
selection: $timeVal,
label: Text("")){
ForEach(10...120, id: \.self) {
Text("\($0)")
}
}
NavigationLink(destination: SecondView(timerScreenShown: $timerScreenShown, timeVal: timeVal), isActive: $timerScreenShown, label: {Text("Go")})
}
}
}
struct SecondView: View{
#Binding var timerScreenShown:Bool
#State var timeVal = 10
#State var startVal = 0
var body: some View {
VStack{
if timeVal > 0 {
Text("Timer")
.font(.system(size: 14))
Text("\(startVal)")
.font(.system(size: 40))
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > 0{
self.startVal += 1
if self.timeVal == self.startVal{
WKInterfaceDevice.current().play(.failure)
self.startVal = 0
}
}
}
}
Text("seconds")
.font(.system(size: 14))
Button(action: {
self.timerScreenShown = false
}) {
Text("Cancel")
.foregroundColor(.red)
}
} else {
Button(action: {
self.timerScreenShown = false
}) {
Text("Done")
.foregroundColor(.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}

Cannot get Haptic Feedback to work with Swift

I'm a CS major in college who had an idea for an Apple Watch app that vibrates every time it reaches a certain time interval, for example the user picks intervals of 20s through a picker, taps "start" and the app starts a timer from 0 to 20 and once it reaches 20 I want the watch to vibrate and start counting up to 20 again until the user chooses to stop it. I want to use it for runners to have an idea of consistent paces to run. The problem is that I can't get UIImpactFeedbackGenerator to work and none of the other methods of haptic feedback either. I have no swift experience, only python and Java but I am basically done except for this haptic part. The one error I get is UIImpactFeedbackGenerator is out of scope where the generator is declared under SecondView. Thank you in advance! Here's my code:
import SwiftUI
struct ContentView: View {
#State var timerScreenShown = false
#State var timeVal = 10
var body: some View {
VStack{
Text("Select \(timeVal)s intervals").padding()
Picker(
selection: $timeVal,
label: Text("")){
ForEach(10...120, id: \.self) {
Text("\($0)")
}
}
NavigationLink(destination: SecondView(timerScreenShown: $timerScreenShown, timeVal: timeVal), isActive: $timerScreenShown, label: {Text("Go")})
}
}
}
struct SecondView: View{
#Binding var timerScreenShown:Bool
#State var timeVal = 10
#State var startVal = 0
var body: some View {
VStack{
if timeVal > 0 {
Text("Timer")
.font(.system(size: 14))
Text("\(startVal)")
.font(.system(size: 40))
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > 0{
self.startVal += 1
if self.timeVal == self.startVal{
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
self.startVal = 0
}
}
}
}
Text("seconds")
.font(.system(size: 14))
Button(action: {
self.timerScreenShown = false
}) {
Text("Cancel")
.foregroundColor(.red)
}
} else {
Button(action: {
self.timerScreenShown = false
}) {
Text("Done")
.foregroundColor(.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
WatchOs does have the play(_:) - method for notification.
try WKInterfaceDevice.current().play(.click)
for more information take a look at the documentation -- https://developer.apple.com/documentation/watchkit/wkinterfacedevice/1628128-play

Pickers are overlapping in ios 15 preventing some of them to be scrolled

I Created a custom time picker in Swiftui to enter a desired time, the picker works by having multiple picker next to each other, to select time values, ie there is a picker for hours, minutes, seconds and miliseconds. With iOS 14 this worked perfectly but since updating to iOS 15 only one picker works at a time. This seems to be because the pickers are now overlapping. To me it seems that setting the frame is not working properly, but I am unsure how to fix this issue.
The code consists of a TimePickerClass which stores the values of the time pickers:
class TimePickerClass: ObservableObject {
#Published var hoursTime = 0
#Published var secondsTime = 0
#Published var milisecondsTime = 0
#Published var minutesTime : Int = 0
func GetTime() -> Double {
return Double(hoursTime) * 3600.0 + Double(minutesTime) * 60.0 + Double(secondsTime) + Double(milisecondsTime)/100
}
func GetTimeString() -> String {
return TimeFormatted(timeInSeconds: GetTime())
}
func Reset(){
hoursTime = 0
secondsTime = 0
milisecondsTime = 0
minutesTime = 0
}
}
And this is the TimePicker view
struct TimePicker: View {
#ObservedObject var viewModel : TimePickerClass
let pickerColor : Color = AppColor.PickerColors.backgroundColor
let textColor : Color = AppColor.PickerColors.textColor
let width :CGFloat = 30
var body: some View {
HStack
{
HStack(alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, spacing: 0){
Spacer()
Picker("", selection: $viewModel.hoursTime) {
ForEach(0..<24){ hours in
if(hours < 10){
Text(" \(hours)").foregroundColor(textColor)
}
else{
Text("\(hours)").foregroundColor(textColor)
}
}
}.pickerStyle(WheelPickerStyle()).frame(width: width, height: 40).clipped().labelsHidden().clipShape(Rectangle())
Text(":").foregroundColor(textColor)
Picker("", selection: $viewModel.minutesTime) {
ForEach(0..<60){ minutes in
if(minutes < 10){
Text("0\(minutes)").foregroundColor(textColor)
}
else{
Text("\(minutes)").foregroundColor(textColor)
}
}
}.pickerStyle(WheelPickerStyle()).frame(width: width, height: 40).clipped().labelsHidden().clipShape(Rectangle())
Text(":").foregroundColor(textColor)
Picker("", selection: $viewModel.secondsTime) {
ForEach(0..<60){ seconds in
if(seconds < 10){
Text("0\(seconds)").foregroundColor(textColor)
}
else{
Text("\(seconds)").foregroundColor(textColor)
}
}
}.pickerStyle(WheelPickerStyle()).frame(width: width, height: 40).clipped().labelsHidden().clipShape(Rectangle())
Text(".").foregroundColor(textColor)
Picker("", selection: $viewModel.milisecondsTime) {
ForEach(0..<100){ miliSeconds in
if(miliSeconds < 10){
Text("0\(miliSeconds)").foregroundColor(textColor)
}
else{
Text("\(miliSeconds)").foregroundColor(textColor)
}
}
}.pickerStyle(WheelPickerStyle())
.labelsHidden()
.frame(width: width, height: 40)
.clipped()
.clipShape(Rectangle())
Spacer()
}.padding(.leading).padding(.trailing).overlay(RoundedRectangle(cornerRadius: 10).stroke(pickerColor, lineWidth: 2))
}.background(pickerColor.clipShape(RoundedRectangle(cornerRadius: 10))).overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.black, lineWidth: 1))
}
}
To test it use:
struct ContentView: View {
#ObservedObject var testTimePicker = TimePickerClass()
var body: some View {
VStack{
TimePicker(viewModel: testTimePicker)
Text("\(String(testTimePicker.GetTime()))")
Text(testTimePicker.GetTimeString())
}
}
iOS 14
iOS 15
try adding
.compositingGroup()
.clipped()
to your Pickers.

ProgressBar iOS 13

I've tried creating my own ProgressView to support iOS 13, but for some reason it appears to not work. I've tried #State, #Binding and the plain var progress: Progress, but it doesn't update at all.
struct ProgressBar: View {
#Binding var progress: Progress
var body: some View {
VStack(alignment: .leading) {
Text("\(Int(progress.fractionCompleted))% completed")
ZStack {
RoundedRectangle(cornerRadius: 2)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 4)
GeometryReader { metrics in
RoundedRectangle(cornerRadius: 2)
.foregroundColor(.blue)
.frame(width: metrics.size.width * CGFloat(progress.fractionCompleted))
}
}.frame(height: 4)
Text("\(progress.completedUnitCount) of \(progress.totalUnitCount)")
.font(.footnote)
.foregroundColor(.gray)
}
}
}
In my content view I added both the iOS 14 variant and the iOS 13 supporting one. They look the same, but the iOS 13 variant does not change anything.
struct ContentView: View {
#State private var progress = Progress(totalUnitCount: 10)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
ProgressBar(progress: $progress)
.padding()
.onReceive(timer) { timer in
progress.completedUnitCount += 1
if progress.isFinished {
self.timer.upstream.connect().cancel()
progress.totalUnitCount = 500
}
}
if #available(iOS 14, *) {
ProgressView(progress)
.padding()
.onReceive(timer) { timer in
progress.completedUnitCount += 1
if progress.isFinished {
self.timer.upstream.connect().cancel()
progress.totalUnitCount = 500
}
}
}
}
}
}
The iOS 14 variant works, but my iOS 13 implementation fails. Can somebody help me?
Progress is-a NSObject, it is not a struct, so state does not work for it. You have to use KVO to observe changes in Progress and redirect into SwiftUI view's source of truth.
Here is a simplified demo of possible solution. Tested with Xcode 12.1 / iOS 14.1
class ProgressBarViewModel: ObservableObject {
#Published var fractionCompleted: Double
let progress: Progress
private var observer: NSKeyValueObservation!
init(_ progress: Progress) {
self.progress = progress
self.fractionCompleted = progress.fractionCompleted
observer = progress.observe(\.completedUnitCount) { [weak self] (sender, _) in
self?.fractionCompleted = sender.fractionCompleted
}
}
}
struct ProgressBar: View {
#ObservedObject private var vm: ProgressBarViewModel
init(_ progress: Progress) {
self.vm = ProgressBarViewModel(progress)
}
var body: some View {
VStack(alignment: .leading) {
Text("\(Int(vm.fractionCompleted * 100))% completed")
ZStack {
RoundedRectangle(cornerRadius: 2)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 4)
GeometryReader { metrics in
RoundedRectangle(cornerRadius: 2)
.foregroundColor(.blue)
.frame(width: metrics.size.width * CGFloat(vm.fractionCompleted))
}
}.frame(height: 4)
Text("\(vm.progress.completedUnitCount) of \(vm.progress.totalUnitCount)")
.font(.footnote)
.foregroundColor(.gray)
}
}
}
and updated call place to use same constructor
struct ContentView: View {
#State private var progress = Progress(totalUnitCount: 10)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
ProgressBar(progress) // << here !!
// ... other code