SwiftUI: Compact DatePicker with custom design - swiftui

I need a custom "label" with the behavior of opening the date picker in some kind of popover, even on the iPhone.
When I use the built in DatePicker with compact format, I have the behavior with opening the wheels in a popover, but I cannot customize the look of the "collapsed" version with the gray background (in fact, I can change it to some other color, but not to white e.g., it simply doesn't work).
Another option would be to have a custom Text wrapped inside a Button and then open the DatePicker, but I don't know how to achieve the popover effect on the iPhone. So I guess the first option would be the easier one, but how can I achieve a custom layout for the compact date picker? Nothing super fancy, just something like this:
instead of something like this

its more of a proof of concept, but this might be a way:
import SwiftUI
struct ContentView: View {
#State private var showPicker = true
#State var selectedDate = Date()
Button {
withAnimation {
showPicker.toggle()
}
} label: {
Text(Text(selectedDate.getFormattedDate(format:"HH:mm:")))
.padding()
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray)
)
}
.background(
DatePicker("", selection: $selectedDate, displayedComponents: .hourAndMinute)
.datePickerStyle(.wheel)
.frame(width: 200, height: 100)
.clipped()
.background(Color.gray.cornerRadius(10))
.opacity(showPicker ? 1 : 0 )
.offset(x: 50, y: 90)
).onChange(of: selectedDate) { newValue in
withAnimation {
showPicker.toggle()
}
}
}
Note that DidSet or willSet is not possible for Objects, however, we had a nice workaround onChange. When date changed, just toggle, done.

Related

SwiftUI - Custom DatePicker - Show layer or backgorund on top of all or how to do it as DatePicker

I'm trying two write a custom Datepicker so that I can modify the button style. As far as I know, SwiftUI doesn't allow to modify it. My base design based on this answer that I've already improved it.
struct CustomDatePickerView: View {
#State private var showPicker = false
#State var selectedText: String
#State var selectedDateLocal : Date = Date()
var body: some View {
VStack {
Button {
withAnimation {
showPicker.toggle()
}
} label: {
Text(selectedText)
.padding()
.padding(.horizontal)
.foregroundColor(.black)
.background(
RoundedRectangle( cornerRadius:10, style: .continuous).fill(Color.yellow.opacity(0.2))
)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(Color.black, lineWidth: 1)
)
.id(selectedText)
}
.background(
DatePicker("", selection: $selectedDateLocal, in: closedRange, displayedComponents: [.date,.hourAndMinute])
.datePickerStyle(.graphical)
.frame(width: 400, height: 400)
.clipped()
.background(Color.yellow.opacity(0.1).cornerRadius(10))
.opacity(showPicker ? 1 : 0 )
.offset(x: 0, y: 230)
).onChange(of: selectedDateLocal) { newValue in
let format = DateFormatter()
format.timeStyle = .none
format.dateStyle = .short
print("Name changed to \(format.string(from: newValue))!")
selectedText = format.string(from: newValue)
withAnimation {
showPicker.toggle()
}
}
}
}
}
It was working excellent on test view (sorry image upload failure).
When it is placed on the real application, the background/overlay was behind of other views.
I cannot change the Z-level since the UI a bit complicated.
How can we show the DatePicker displayed on background/overlay on top of everything as the DatePicker does? What is the proper way to do this?

Radiobuttons in List swiftUI

Im displaying data from server and need to display the options in radiobuttons. But default none of the button should be selected. I was able to display radiobuttons. But when a particular button is clicked, only its image should be changed. Though with my code all the other button images too change. I have gone through some references SwiftUI - How to change the button's image on click?, but couldn't get it work. As am a newbie to SwiftUI, strucked here.
struct ListView: View {
#State var imageName: String = "radio-off"
var body: some View {
List(vwModel.OpnChoice.ItemList) { opn in
VStack(alignment:.leading) {
ForEach(opn.Choices.indices, id: \.self) { row in
Button(action: {
print("\(opn.Choices[row].ChoiceId)")
self.imageName = "radio-On"
}) {
Image(imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
Text(opn.Choices[row].ChoiceName)
}
}
}
}
}
As you are working with indices anyway, add a #Published property to your view model which contains the selected index.
Then set the index in the button action. The redraw of the view sets the button at the selected index to the on-state and the others to the off-state.
As I don't know your environment this is a simplified stand-alone view model and view with SF Symbols images
class VMModel : ObservableObject {
#Published var selectedOption = -1
var numberOfOptions = 5
}
struct ListView: View {
#StateObject private var vwModel = VMModel()
var body: some View {
VStack(alignment:.leading) {
ForEach(0..<vwModel.numberOfOptions, id: \.self) { opn in
Button(action: {
vwModel.selectedOption = opn
print("index \(opn) selected")
}) {
Image(systemName: opn == vwModel.selectedOption ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
}
}
}
}
Update:
Meanwhile you can use a Picker with the .radioGroup modifier
enum Choice {
case one, two, three
}
struct ListView: View {
#State private var choice : Choice = .one
var body: some View {
Picker(selection: $choice, label: Text("Select an option:")) {
Text("One").tag(Choice.one)
Text("Two").tag(Choice.two)
Text("Three").tag(Choice.three)
}.pickerStyle(.radioGroup)
}
}
At the moment you are storing a single #State property "imageName" for the entire list. And then when any of the buttons in the list are tapped you are changing that single property. Which will affect all the buttons.
I'd suggest removing this property and putting something into the viewModel.
You appear to have a view model with an array called vwModel.opnChoice.itemList.
There are multiple ways of making this work. You could stop a boolean in the itemList to say whether each item is selected or not. But, as you want it to work like radio buttons what might be better is to have a property on the vwModel like selectedItem.
The button could then do...
vwModel.selectedItemId = opn.choices[row].choiceId
Then you button Label would be...
Image(vsModel.selectedItemId == opn.choices[row].choiceId ? "radio-on" : "radio-off")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
This would allow you to toggle the button when it is tapped and should turn the other buttons off.

SwiftUI: presenting modal view with custom transition from frame to frame?

I am trying to replicate what I used to do in UIKit when presenting a ViewController using UIViewControllerTransitioningDelegate and UIViewControllerAnimatedTransitioning.
So, for example, from a view that looks like this:
I want to present a view (I would say a modal view but I am not sure if that is the correct way to go about it in SwiftUI) that grows from the view A into this:
So, I need view B to fade in growing from a frame matching view A into almost full screen. The idea is the user taps on A as if it wanted to expand it into its details (view B).
I looked into SwiftUI transitions, things like this:
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
So, I think I need to build a custom transition. But, I am not sure how to go about it yet being new to this.
How would I build a transition to handle the case as described? Being able to have a from frame and a to frame...?
Is this the right way of thinking about it in SwiftUI?
New information:
I have tested matchedGeometryEffect.
Example:
struct TestParentView: View {
#State private var expand = false
#Namespace private var shapeTransition
var body: some View {
VStack {
if expand {
// Rounded Rectangle
Spacer()
RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300)
.padding()
.foregroundColor(Color(.systemGreen))
.animation(.easeIn)
.onTapGesture {
expand.toggle()
}
} else {
// Circle
RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(width: 100, height: 100)
.foregroundColor(Color(.systemOrange))
.animation(.easeIn)
.onTapGesture {
expand.toggle()
}
Spacer()
}
}
}
}
It looks like matchedGeometryEffect could be the tool for the job.
However, even when using matchedGeometryEffect, I still can't solve these two things:
how do I include a fade in / fade out animation?
looking at the behavior of matchedGeometryEffect, when I "close" view B, view B disappears immediately and what we see animating is view A from where B was back to view A's original frame. I actually want view B to scale down to where A is as it fades out.
You would have to use the .matchedGeometryEffect modifier on the two Views that you would like to transition.
Here is an example:
struct MatchedGeometryEffect: View {
#Namespace var nspace
#State private var toggle: Bool = false
var body: some View {
HStack {
if toggle {
VStack {
Rectangle()
.foregroundColor(Color.green)
.matchedGeometryEffect(id: "animation", in: nspace)
.frame(width: 300, height: 300)
Spacer()
}
}
if !toggle {
VStack {
Spacer()
Rectangle()
.foregroundColor(Color.blue)
.matchedGeometryEffect(id: "animation", in: nspace)
.frame(width: 50, height: 50)
}
}
}
.padding()
.overlay(
Button("Switch") { withAnimation(.easeIn(duration: 2)) { toggle.toggle() } }
)
}
}
Image should be a GIF
The main two parts of using this modifier are the id and the namespace.
The id of the two Views you are trying to match have to be the same. They then also have to be in the same namespace. The namespace is declared at the top using the #Namespace property wrapper. In my example I used "animation", but it can really be anything, preferably something that can uniquely identify the Views from other types of animations.
Another important piece of information is that the '''#State''' variable controlling the showing/hiding of Views is animated. This is done through the use of withAnimation { toggle.toggle() }.
I'm also quite new to this, so for some more information you can read this article I found from the Swift-UI Lab:
https://swiftui-lab.com/matchedgeometryeffect-part1/

How to get a horizontal picker for Swift UI?

Is there an existing solution to get a horizontal picker in Swift UI?
Example for Swift: https://github.com/akkyie/AKPickerView-Swift
I ended up solving this using a horizontal scrollview, tap gestures, and state to track the selection.
#State private var index
var body: some View {
        return ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(0..<self.items.count, id: \.self) { i in
                    Text("\(i)")
                        .foregroundColor(self.index == i ? .red : .black)
                        .frame(width: 20, height: 20)
                        .gesture(TapGesture().onEnded({ self.index = i }))
                }
            }
        }
        .frame(width: self.width, alignment: .leading)
}
You can use the Rotation Effect on a Picker and on the Picker's content. Make sure to rotate the content 90 degrees opposite from the picker or else the content will be sideways. If the picker is too big, you can manually set a height of the picker by using frame(height: _). In my case, I used frame(maxHeight: _). You might need to adjust the row indicators by using clipped() after the resize to stop them from flowing out of the picker.
I'm using images as an example but it should work with most if not all basic views.
Code:
Picker(selection: $data, label: Text("Data")) {
ForEach(dataArray, id: \.self) { imageName in
Image(imageName)
.resizable()
.scaledToFit()
.rotationEffect(Angle(degrees: 90))
}
}
.labelsHidden()
.rotationEffect(Angle(degrees: -90))
.frame(maxHeight: 100)
.clipped()

SwiftUI - switch toolbar states

Goal: a toolbar that switches states, according to object selected (image, text, shape)
What I did:
iimport SwiftUI
struct ToolbarMaster : View {
#State var showtoolbar = false
var toolbarmaster: [ToolbarBezier] = []
var body: some View {
HStack {
Spacer()
VStack {
Button(action: {self.showtoolbar.toggle() }) {
Image(systemName: "gear")
}
.padding(.leading)
Image("dog")
Text("Im a text")
.font(.largeTitle)
.color(.black)
Path(ellipseIn: CGRect(x: 0, y: 0, width: 100, height: 100))
.fill(Color.black)
}
NavigationView {
ZStack {
ToolbarBezier()
ToolbarArtwork()
}
.navigationBarTitle(Text("Toolbar Name"), displayMode: .inline)
}
.frame(width: 320.0)
}
}
}
My result:
How can I change the state, while selecting different objects?
I need to do in a dynamic way (not hardcoded), so that when any object that is an image, it will display Image Toolbar, and so on.
I would do it like this :
Add a #State boolean variable for each state of your toolbar
#State private var textSelected = false
Add an .onTap event to each of the "active" views on your scene :
Text("Tap me!")
.tapAction {
self.textSelected = true
}
Make the appearance of you toolbar change based on the "state" variable(s)
if self.textSelected {
self.showTextToolbarContent()
}
Of course, showTextToolbarContent() is a local function returning a some View with the toolbar content tailored for text selection.
This is obviously only a bare-bone example. In your actual implementation you'll probably use an array of bools for the "selection" state and another state var for the view currently selected.