SwiftUI - Changing GraphicalDatePickerStyle DatePicker's visibility causes memory leak - ios

I have an app that has a datepicker (GraphicalDatePickerStyle). It hidden by default. When you tap the view that contains it, it becomes visible.
DateTimePicker
struct DateTimePicker<Content: View>: View {
#Binding var selection: Date
#State private var isDatePickerVisible: Bool = false
private var displayedComponents: DatePickerComponents
private var content: (_ isVisible: Bool) -> Content
init(selection: Binding<Date>,
displayedComponents: DatePickerComponents = [.date],
content: #escaping (_ isVisible: Bool) -> Content) {
self._selection = selection
self.content = content
self.displayedComponents = displayedComponents
}
var body: some View {
VStack(alignment: .center) {
self.content(isDatePickerVisible)
.onTapGesture {
withAnimation {
self.isDatePickerVisible.toggle()
}
}
if isDatePickerVisible {
VStack {
DatePicker("",
selection: $selection,
displayedComponents: self.displayedComponents)
.labelsHidden()
.datePickerStyle(GraphicalDatePickerStyle())
}
}
}
}
}
ContentView that contains DateTimePicker
struct ContentView: View {
#State var selection = Date()
var body: some View {
ScrollView(.vertical) {
DateTimePicker(selection: $selection) { _ in
HStack {
Text("Date ")
Spacer()
Text("\(selection)")
}
}
}
}
}
There are a few memory leaks if you show and hide DateTimePicker. I tried a lot but could not fix it.

Related

SwiftUI custom TabView with paging style

I'm trying to create a custom TabView in SwiftUI, that also has a .tabViewStyle(.page()) functionality too.
At the moment I'm 99% of the way there, but cannot figure out how to get all the TabBarItems to list.
I'm using the PreferenceKey so that the order I add them into the closure is the order in the TabView.
When I run it, the tab items are added to the array, then removed, and it doesn't seem to be working.
I had it working with the enum as CaseIterable and the ForEach(tabs) { tab in as ForEach(TabBarItems.allCases) { tab in, but as mentioned wanted the order in the bar to be organic from the clousure.
Container
struct TabViewContainer<Content : View>: View {
#Binding private var selection: TabBarItem
#State private var tabs: [TabBarItem] = []
var content: Content
init(selection: Binding<TabBarItem>, #ViewBuilder content: () -> Content) {
self._selection = selection
self.content = content()
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
content
}
.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
.onPreferenceChange(TabBarItemsPreferenceKey.self) { self.tabs = $0 }
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}
PreferenceKey / Modifier
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
struct TabBarItemViewModifier: ViewModifier {
let tab: TabBarItem
func body(content: Content) -> some View {
content.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(_ tab: TabBarItem) -> some View {
modifier(TabBarItemViewModifier(tab: tab))
}
}
Demo view
struct TabSelectionView: View {
#State private var selection: TabBarItem = .itinerary
var body: some View {
TabViewContainer(selection: $selection) {
PhraseView()
.tabBarItem(.phrases)
ItineraryView()
.tabBarItem(.itinerary)
BudgetView()
.tabBarItem(.budget)
BookingView()
.tabBarItem(.bookings)
PackingListView()
.tabBarItem(.packing)
}
}
}
Intended
Current
You can use a more elegant way, #resultBuilder:
You create a struct that holds the View & the tag;
tabBarItem should now return the previously created struct;
The #resultBuilder will then build your array of your view & tag which you'll be using inside the container.
ResultBuilder:
#resultBuilder
public struct TabsBuilder {
internal static func buildBlock(_ components: Tab...) -> [Tab] {
return components
}
internal static func buildEither(first component: Tab) -> Tab {
return component
}
internal static func buildEither(second component: Tab) -> Tab {
return component
}
}
Tab:
struct Tab: Identifiable {
var content: AnyView //I don't recommend the use of AnyView, but I don't want to dive deep into generics for now.
var tag: TabBarItem
var id = UUID()
}
Modifier:
struct Tab: Identifiable {
var content: AnyView
var tag: TabBarItem
var id = UUID()
}
TabViewContainer:
struct TabViewContainer: View {
#Binding private var selection: TabBarItem
#State private var tabs: [TabBarItem]
var content: [Tab]
init(selection: Binding<TabBarItem>, #TabsBuilder content: () -> [Tab]) {
self._selection = selection
self.content = content()
self.tabs = self.content.map({$0.tag})
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
ForEach(content) { content in
content.content
.tag(content.tag)
}
}.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}

How can I go to a view by clicking on a Button in SwiftUI

If the user clicks on connexionBtnView I want to redirect them to an AdminView or UserView
import SwiftUI
struct ConnexionView: View {
#State var loginId: String = ""
#State var pwd: String = ""
#StateObject private var keyboardHander = KeyBoardHandler()
var body: some View {
NavigationView{
ZStack{
Image("background")
.ignoresSafeArea()
VStack (spacing: 15){
Spacer()
logoView
Spacer()
titleView
loginIdView
loginPwdView
connexionBtnView
Spacer()
NavigationLink {
LostPwdView()
} label: {
lostPwd
}
Spacer()
}.frame(maxHeight: .infinity)
.padding(.bottom,keyboardHander.keyboardHeight)
.animation(.default)
}
}
}
The NavigationLink has the isActive parameter. You can pass it in the init of NavigationLink and when this state variable has the true value you will redirect to another view. Details here.
struct ConnexionView: View {
#State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $isActive) {
LostPwdView()
} label: {
Text("Some Label")
}
Button("Tap me!") {
isActive = true
}
}
}
}
}
struct LostPwdView: View {
var body: some View {
Text("Hello")
}
}
What you need to do is having a #State variable that would trigger the navigation:
.fullScreenCover(
isPresented: $viewShown
) {
print("View dismissed")
} content: {
NextView()
}
Where NextView() is the View you want to show and the viewShown is your State variable, below a full example:
struct ExampleView: View {
#State var isNextPageOpen = false
var body: some View {
Button("Tap Here To Navigate") {
isNextPageOpen = true
}
.fullScreenCover(
isPresented: $isNextPageOpen
) {
print("View dismissed")
} content: {
NextView()
}
}
}

How to make a button (or any other element) show SwiftUI's DatePicker popup on tap?

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?
I'm having this problem too. I couldn't sleep for a few days thinking about the solution. 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) {}
}
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) {}
}
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
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())})
}
}
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
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) {}
}

ScrollView and .sheet

I have a ScrollView with a ForEach loop, each rendering a View. In the View I have 3 renders of the below ActionItem (a button that displays a sheet). The sheet does not show up with ScrollView but does with List. I'd normally attach the .sheet at the ScrollView layer however, with each button rendering a different view it seems more appropriate to nest it.
How I could get this to work with ScrollView? I'm using Xcode 12
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: () -> Content) {
self.text = text
self.icon = icon
self.content = content()
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content
}
}
}
In the View I'd render ActionItem such as Text, this also occurs if the View is ignored and the ActionItem is just directly in the ForEach. Same issue, sheet does not appear.
ActionItem(text: "", icon: "pencil") {
Text("ok")
}
The list looks like this
import SwiftUI
struct ItemsList: View {
#ObservedObject var itemModel: ItemModel
var body: some View {
VStack(alignment: .center, spacing: 0) {
ScrollView {
VStack {
ForEach(itemModel.items, id: \.self) { item in
ItemView(item: item)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}}
Suggested callback update
struct ActionItem<Content>: View where Content : View {
public var text: String
public var icon: String
public var content: () -> Content
#State var isPresented = false
init(text: String, icon: String, #ViewBuilder content: #escaping () -> Content) {
self.text = text
self.icon = icon
self.content = content
}
var body: some View {
Button (action: {
DispatchQueue.main.async {
withAnimation {
self.isPresented = true
}
}
}) {
HStack(spacing: 2) {
Image(systemName: icon).font(.system(size: 14, weight: .semibold))
Text(text).fontWeight(.semibold)
}.padding([.top, .bottom], Dimensions.spacing)
.padding([.leading, .trailing], Dimensions.spacingMedium)
}.foregroundColor(Color.gray).font(.subheadline).background(Color.grayWhiteTer)
.cornerRadius(Dimensions.spacing)
.sheet(isPresented: $isPresented) {
self.content()
}
}
}
Try saving content as a callback (i.e. () -> Content) and call it in the sheet method instead of calling it in the initializer.. This will change when the view is created.

SwiftUI - Animate View expansion (show / hide)

I have a View that contains a HStack and a DatePicker. When you tap on the HStack, the DatePicker is shown / hidden. I want to animate this action like the animation of Starts and Ends row in iOS Calendar's New Event View.
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time",
leadingText: "Start Time",
trailingText: startTime.stringTime())
}
.onTapGesture {
withAnimation {
self.isDatePickerVisible.toggle()
}
}
Group {
if isDatePickerVisible {
DatePicker("", selection: $startTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(WheelDatePickerStyle())
}
}
.background(Color.red)
.modifier(AnimatingCellHeight(height: isDatePickerVisible ? 300 : 0))
}
}
}
I have used the following code for animation. It almost works. The only problem is that HStack jumps. And I can not fix it.
https://stackoverflow.com/a/60873883/8292178
struct AnimatingCellHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}
How to fix this issue? How to animate visibility of the DatePicker?
It's simple, you don't need extra ViewModifier
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time"
, leadingText: "Start Time"
, trailingText: startTime.stringTime())
}.onTapGesture {
isDatePickerVisible.toggle()
}
if isDatePickerVisible {
DatePicker(""
, selection: $model.startTime
, displayedComponents: [.hourAndMinute]
).datePickerStyle(WheelDatePickerStyle())
}
}.animation(.spring())
}
}

Resources