I've created a view for setting a time (minutes and seconds). It uses two wheel pickers bound to two state variables.
Now I'd like to use that view at different places in the app, but I don't like the interface with two seperate variables for the time. Instead, I'd like to have just one bound variable holding the time in seconds (so time = 185 would translate to 3 minutes and 5 seconds).
Is it possible to have some sort of "adapter" between bindings?
Here's the view:
import SwiftUI
struct TimePicker: View {
var minutes: Binding<Int>
var seconds: Binding<Int>
var body: some View {
HStack() {
Spacer()
Picker(selection: minutes, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Min.")
Picker(selection: seconds, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Sec.")
Spacer()
}
}
}
Here is the approach based on the Binding(get:set:)
struct TimePicker: View {
#Binding var total: Int
var minutes: Binding<Int> {
Binding<Int>(get: { self._total.wrappedValue / 60 },
set: { self._total.wrappedValue = self._total.wrappedValue % 60 + $0 * 60 })
}
var seconds: Binding<Int> {
Binding<Int>(get: { self._total.wrappedValue % 60 },
set: { self._total.wrappedValue = (self._total.wrappedValue / 60) * 60 + $0 })
}
var body: some View {
HStack() {
Spacer()
Picker(selection: minutes, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Min.")
Picker(selection: seconds, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.pickerStyle(WheelPickerStyle()).frame(width: 50).clipped()
Text("Sec.")
Spacer()
}.frame(height: 200)
}
}
struct TestTimePicker: View {
#State var seconds = 185
var body: some View {
VStack {
Text("Current: \(seconds)")
TimePicker(total: $seconds)
}
}
}
Related
With some items of my picker, the text of the selected item takes 2 lines, how can I avoid this ?
There is my code :
struct PickerTestView: View {
#AppStorage("firstNotificationSelection") var firstNotificationSelection: String = (UserDefaults.standard.string(forKey: "firstNotificationSelection")) ?? "None"
#AppStorage("secondNotificationSelection") var secondNotificationSelection: String = (UserDefaults.standard.string(forKey: "secondNotificationSelection")) ?? "None"
let notificationChoices: [String] = ["None", "At time of match", "5 minutes before...", "10 minutes before...", "30 minutes before...", "1 hour before...", "2 hours before..."]
var body: some View {
NavigationStack {
Form {
Section {
Picker(selection: $firstNotificationSelection) {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
} label: {
Text("Alert")
}
if firstNotificationSelection != "None" {
Picker(selection: $secondNotificationSelection) {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
} label: {
Text("Second alert")
}
}
}
}
.navigationTitle("Notifications")
.navigationBarTitleDisplayMode(.inline)
}
}
}
I tried with the modifier .lineLimit(1) and to fix the frame of the Picker but it has no effect.
you have more control if you wrap your picker inside a Menu
Menu {
Picker(selection: $firstNotificationSelection, label: EmptyView(), content: {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
})
} label: {
Text(firstNotificationSelection)
}
or, in the full context of your example, it would look like this
struct PickerTestView: View {
#AppStorage("firstNotificationSelection") var firstNotificationSelection: String = (UserDefaults.standard.string(forKey: "firstNotificationSelection")) ?? "None"
#AppStorage("secondNotificationSelection") var secondNotificationSelection: String = (UserDefaults.standard.string(forKey: "secondNotificationSelection")) ?? "None"
let notificationChoices: [String] = ["None", "At time of match", "5 minutes before...", "10 minutes before...", "30 minutes before...", "1 hour before...", "2 hours before..."]
var body: some View {
NavigationStack {
Form {
Section {
HStack {
Text("Alert")
Spacer()
Menu {
Picker(selection: $firstNotificationSelection, label: EmptyView(), content: {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
})
} label: {
Text(firstNotificationSelection)
}
}
if firstNotificationSelection != "None" {
HStack {
Text("Second Alert")
Spacer()
Menu {
Picker(selection: $secondNotificationSelection, label: EmptyView(), content: {
ForEach(notificationChoices, id: \.self) { choice in
Text(choice)
}
})
} label: {
Text(secondNotificationSelection)
}
}
}
}
}
.navigationTitle("Notifications")
.navigationBarTitleDisplayMode(.inline)
}
}
}
As of today (feb 2023) the default SwiftUI picker does not have an option to change the number of lines for the selected value text
I have a textfield which is supposed to log the units of a food product someone has eaten, which is then used to calculate the total number of calories, protein, etc. that the user consumed. But when the value is entered on the textfield, the units variable isn't updated. How can I fix this?
This is my code:
#State var selectFood = 0
#State var units = 0
#State var quantity = 1.0
#State var caloriesInput = 0.0
#State var proteinInput = 0.0
#State var carbsInput = 0.0
#State var fatsInput = 0.0
var body: some View {
VStack {
Group {
Picker(selection: $selectFood, label: Text("What did you eat?")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white))
{
ForEach(database.productList.indices, id: \.self) { i in
Text(database.productList[i].name)
}
}
.pickerStyle(MenuPickerStyle())
Spacer(minLength: 25)
Text("How much did you have?")
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(alignment: .leading)
//Textfield not working.
TextField("Units", value: $units, formatter: NumberFormatter())
.padding(10)
.background(Color("Settings"))
.cornerRadius(10)
.foregroundColor(Color("Background"))
.keyboardType(.numberPad)
Button (action: {
self.quantity = ((database.productList[selectFood].weight) * Double(self.units)) / 100
caloriesInput = database.productList[selectFood].calories * quantity
proteinInput = database.productList[selectFood].protein * quantity
carbsInput = database.productList[selectFood].carbs * quantity
fatsInput = database.productList[selectFood].fats * quantity
UIApplication.shared.hideKeyboard()
}) {
ZStack {
Rectangle()
.frame(width: 90, height: 40, alignment: .center)
.background(Color(.black))
.opacity(0.20)
.cornerRadius(15)
;
Text("Enter")
.foregroundColor(.white)
.fontWeight(.bold)
}
}
}
}
}
This is an issue with NumberFormatter that has been going on for a while. If you remove the formatter it updates correctly.
This is a workaround. Sadly it requires 2 variables.
import SwiftUI
struct TFConnection: View {
#State var unitsD: Double = 0
#State var unitsS = ""
var body: some View {
VStack{
//value does not get extracted properly
TextField("units", text: Binding<String>(
get: { unitsS },
set: {
if let value = NumberFormatter().number(from: $0) {
print("valid value")
self.unitsD = value.doubleValue
}else{
unitsS = $0
//Remove the invalid character it is not flawless the user can move to in-between the string
unitsS.removeLast()
print(unitsS)
}
}))
Button("enter"){
print("enter action")
print(unitsD.description)
}
}
}
}
struct TFConnection_Previews: PreviewProvider {
static var previews: some View {
TFConnection()
}
}
The following code creates new controls every time a button is pressed at runtime, the problem is that the picker selection is set to the same state.
How can I create new controls with different state variables so they can operate separately ?
struct ContentView: View {
#State private var numberOfControlls = 0
#State var selection: String="1"
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
Picker(selection: self.$selection, label:
Text("Picker") {
Text("1").tag(1)
Text("2").tag(2)
}
}
}
}
}
How can I create new controls with different state variables so they can operate separately ?
Separate control into standalone view with own state (or view model if/when needed).
Here is a demo:
struct ContentView: View {
#State private var numberOfControlls = 0
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
ControlView()
}
}
}
}
struct ControlView: View {
#State var selection: String="1"
var body: some View {
Picker(selection: self.$selection, label:
Text("Picker")) {
Text("1").tag(1)
Text("2").tag(2)
}
}
}
I'm using a tab view using the UIPageViewController behaviour. So I defined the following model:
class WalktroughModel: Identifiable, ObservableObject {
let id: UUID = UUID()
let imageName: String
let title: String
init(imageName: String, title: String) {
self.imageName = imageName
self.title = title
}
}
Now I use this swiftUI view as a child view of tab view:
struct WalktroughAsset: View {
#StateObject var asset: WalktroughModel
var body: some View {
Image(asset.imageName)
.resizable()
.overlay(Color.black.opacity(0.43))
.overlay(
VStack{
Spacer()
Text(asset.title)
.foregroundColor(.white)
.font(.custom("OpenSans-regular", size: 22.0))
}
.padding(.bottom, 64)
)
}
}
In my content view I have the following:
struct ContentView: View {
var thumbs: [WalktroughModel] = [WalktroughModel(imageName: "splash-1", title: "Concepto 1"), WalktroughModel(imageName: "splash-2", title: "Concepto 2"), WalktroughModel(imageName: "splash-3", title: "Concepto 3")]
var body: some View {
ZStack {
Color.black.overlay(TabView{
ForEach(thumbs) {
image in
WalktroughAsset(asset: image)
}
}
.tabViewStyle(PageTabViewStyle())
.padding([.bottom, .top], 32)
)
}
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
}
Now, when I build and run the memory jumps 80 mb to 160 mb when I swipe to the other view and jumps to 230 mb when I swipe to the third view. What could be happen?
Best Regards
This code fixes a memory leak for me
struct TabViewWrapper<Content: View, Selection: Hashable>: View {
#Binding var selection: Selection
#ViewBuilder let content: () -> Content
var body: some View {
TabView(selection: $selection, content: content)
}
}
Replace TabView(selection:) to TabViewWrapper(selection:)
TabViewWrapper(selection: $selection) {
tabContent
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
This bug is presented when you use custom view with args
Similar issue on iOS 15 (Tested on 15.2)
Memory use for the below SwiftUI app will continuously increase over time. The app is simply showing a TabView in a full screen cover, and has a timer that refreshes a countdown every second. If I display a simple text element instead of the TabView, the memory use will behave properly. I submitted a feedback to Apple.
import SwiftUI
class ContentView_manager : ObservableObject {
var view_timer: Timer?
#Published var countdown_txt: String?
func startTimer(){
if self.view_timer == nil {
self.view_timer = Timer(fireAt: Date(), interval: 1, target: self, selector: #selector(self.timer_action), userInfo: nil, repeats: true)
RunLoop.current.add(self.view_timer!, forMode: RunLoop.Mode.common)
}
}
func stopTimer(){
if self.view_timer != nil {
self.view_timer?.invalidate()
self.view_timer = nil
}
}
#objc
func timer_action(){
//update number of seconds until January 2023
print("Timer Action")
self.countdown_txt = String(DateInterval(start: Date(), end: Date(timeIntervalSince1970: 1672527600)).duration)
}
}
#main
struct debugApp: App {
var body: some Scene {
WindowGroup {
ContentView(view_manager:ContentView_manager())
}
}
}
struct ContentView : View {
#ObservedObject var view_manager : ContentView_manager
#State var show_cover : Bool = false
var body: some View {
VStack{
Button(action: {
self.show_cover = true
}, label: {Text("Show screen cover with TabView")})
}
.fullScreenCover(isPresented: self.$show_cover){ScreenCover(view_manager: view_manager)}
}
}
struct ScreenCover: View {
#ObservedObject var view_manager : ContentView_manager
var body: some View {
tab(txt:view_manager.countdown_txt)
.onAppear(perform: {
view_manager.startTimer()
})
.onDisappear(perform: {
view_manager.stopTimer()
})
}
}
struct tab: View {
var txt: String?
var body: some View {
TabView{
Text("First Tab")
.tabItem {
Image(systemName: "1.square.fill")
Text("First")
}
Text("Next year in \(txt ?? "") seconds!")
.tabItem {
Image(systemName: "2.square.fill")
Text("Second")
}
Text("Third tab")
.tabItem {
Image(systemName: "3.square.fill")
Text("Third")
}
}
.tabViewStyle(PageTabViewStyle())
}
}
I have a timer that fires every half second and that leads to the calling of a function that outputs a set of strings that are used to display a countdown to a specific date. It works when I create a new event and then switch over to the tab that contains the information for the countdown, but when I switch back to the add event tab and then back it stops counting down.
The timer is made using this:
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
It runs later using this
ForEach(eventNames.indices, id: \.self) { index in
VStack{
Text("Your event " + "\(self.eventNames[index])" + " is in " + "\(self.string[index])")
.onReceive(self.timer) { input in
self.differenceDate(numbers: index)
}
}
}
And finally, it calls this function
func differenceDate(numbers: Int) {
self.formatter.unitsStyle = .full
self.formatter.allowedUnits = [.day, .hour, .minute, .second]
//self.formatter.maximumUnitCount = 2
self.now = Date();
if self.now > self.eventDates[numbers] {
self.eventNames[numbers] = "";
}
else {
self.string[numbers] = self.formatter.string(from: self.now, to: self.eventDates[numbers]) ?? ""
}
}
This is the full code
import SwiftUI
struct ContentView: View {
#State private var selection = 0
#State private var eventDates = [Date]()
#State private var eventNames = [String]()
#State private var currentName = "";
#State private var counter = 0;
#State private var placeholderText = "Event Name";
#State private var selectedDate = Date();
var numbers = 0;
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
#State var now = Date();
#State var string = [String]();
var formatter = DateComponentsFormatter();
func differenceDate(numbers: Int) {
self.formatter.unitsStyle = .full
self.formatter.allowedUnits = [.day, .hour, .minute, .second]
//self.formatter.maximumUnitCount = 2
self.now = Date();
if self.now > self.eventDates[numbers] {
self.eventNames[numbers] = "";
}
else {
self.string[numbers] = self.formatter.string(from: self.now, to: self.eventDates[numbers]) ?? ""
}
}
var body: some View {
TabView(selection: $selection){
//Page 1
VStack{
Text("Add New Event")
.underline()
.font(.title)
.padding(15)
// .onReceive(self.timer) { input in
// self.differenceDate(numbers: index)
// //}
// }
// .minimumScaleFactor(0.1)
TextField("\(placeholderText)", text: $currentName)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 1)
.padding(5)
)
Text("When is your event?")
DatePicker("Please enter a date", selection: $selectedDate, displayedComponents: .date)
.labelsHidden()
.scaledToFill()
Button(action: {
if self.currentName != "" {
self.eventNames.append(self.currentName)
self.eventDates.append(self.selectedDate)
self.string.append("")
self.currentName = "";
}
})
{
Text("Add Event")
.font(.headline)
.foregroundColor(.black)
}
.padding(25)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 3)
.padding(5)
)
}
//Tab 1
.tabItem {
VStack {
Image(systemName: "calendar")
Text("Add Event")
}
}
.tag(1)
//Page 2
VStack{
Text("Your Events").underline()
.font(.title)
.padding(15)
ForEach(eventNames.indices, id: \.self) { index in
VStack{
Text("Your event " + "\(self.eventNames[index])" + " is in " + "\(self.string[index])")
.onReceive(self.timer) { input in
self.differenceDate(numbers: index)
}
}
}
}
//Tab 2
.font(.title)
.tabItem {
VStack {
Image(systemName: "flame.fill")
Text("Countdowns")
}
}
.tag(0)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I was wondering if there was a workaround or how to keep the timer firing while the tab changes or pause it when the tab changes and then start it again when the tab is swapped back over.
It needs to attach the .onReceive to the TabView and it will be received on all tabs, like
TabView {
...
// << all tab items here
...
}
.onReceive(self.timer) { _ in
self.differenceDate()
}
and iterate indexes inside of handler
func differenceDate() {
for numbers in eventNames.indices {
// current body here
}
}