Simulator Screenshot
In the example above, I have a data model that contains various times for an Itinerary that will be displayed. I would like to highlight the current time with a filled circle:
struct SampleModel: Identifiable {
let id = UUID()
let title: String
let description: String
let time: Date
let icon: String
}
let timeInterval = 1652619600
class Sample: ObservableObject {
#Published var sample2 = [SampleModel(title: "Coffee", description: "Get coffee", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)), icon: "house"),
SampleModel(title: "Coffee", description: "Get coffee", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 30), icon: "house"),
SampleModel(title: "Dinner", description: "Outback Steakhouse", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 60), icon: "person"),
SampleModel(title: "Check Weather", description: "Use app to check weather", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 120), icon: "cloud.sun.fill"),
SampleModel(title: "Go To Work", description: "Leave for work", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 180), icon: "car.fill"),
SampleModel(title: "YouTube", description: "Watch youtube video", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 240), icon: "video.fill"),
SampleModel(title: "Cut Grass", description: "Work in yard", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 300), icon: "speedometer"),
SampleModel(title: "Ride Bike", description: "Go to the trace", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 360), icon: "bicycle"),
SampleModel(title: "Take Pictures", description: "Photograph the moon", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 420), icon: "camera.fill"),
SampleModel(title: "Boat Ride", description: "Go boating", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 480), icon: "ferry.fill"),
SampleModel(title: "Colorado trip", description: "Fly to Erie", time: Date(timeIntervalSince1970: TimeInterval(timeInterval)).adding(minutes: 540), icon: "airplane")]
func isCurrent(date1: Date) -> Bool {
var currentItem: Bool = false
for cTime in sample2 {
if date1 <= cTime.time && date1 <= Date() {
} else {
currentItem = false
}
}
return currentItem
}
}
The data above is just a sample mock data so I can get the UI and the functioning working properly. In my content view I have. foreach loop on the sample data, and I want to highlight the current item with a filled circle. In the isCurrent() function, I can get all items that already have occurred before, but I want to return the most recent.
ForEach(sampleVM.sample2, id: \.id) { index in
Divider()
ItemRowView(isCurrent: sampleVM.isCurrent(date1: index.time), isLastRow: lastRow(item: index.id), time: Date(), sample: index)
}
ItemRowView() is what I am using to display the data.
As the array is sorted you simply need to find the first item/index whose date is greater than the current date
If the index is not found (current date is later than the date of the last item) the item is the last item
If the index is 0 (current date is sooner than the date of the first item) the item is nil
Otherwise the item is the item at index - 1
let currentItem : SampleModel?
if let nextIndex = sample2.firstIndex(where: {$0.time > Date()}) {
currentItem = nextIndex > 0 ? sample2[nextIndex - 1] : nil
} else {
currentItem = sample2.last!
}
Or if the result is the index
let currentIndex: Int?
if let nextIndex = sample2.firstIndex(where: {$0.time > Date()}) {
currentIndex = nextIndex > 0 ? nextIndex - 1 : nil
} else {
currentIndex = sample2.endIndex - 1
}
Related
When the app first launches in macOS (Big Sur), it populates a list with the items saved by the user. When the user clicks on an item on that list, a second view opens up displaying the contents of that item.
Is there a way to select the first item on that list, as if the user clicked it, and display the second view when the app launches? Furthermore, if I delete an item on the list, I can't go back and select the first item on the list and displaying the second view for that item, or if I create new item, same applies, can't select it.
I have tried looking at answers here, like this, and this, and looked and tried code from a variety of places, but I can't get this to work.
So, using the code answered on my previous question, here's how the bare bones app looks like:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(destination: NoteView(note: note)) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
.onAppear() {
print(data.notes.count)
}
}
}
I have tried adding #State var selection: Int? in AllNotes and then changing the list to
List(data.notes, selection: $selection)
and trying with that, but I can't get it to select anything.
Sorry, newbie here on SwiftUI and trying to learn.
Thank you!
You were close. Table view with selection is more about selecting item inside table view, but you need to select NavigationLink to be opened
There's an other initializer to which does exactly what you need. To selection you pass current selected item. To tag you pass current list item, if it's the same as selection, NavigationLink will open
Also you need to store selectedNoteId instead of selectedNote, because this value wouldn't change after your update note properties
Here I'm setting selectedNoteId to first item in onAppear. You had to use DispatchQueue.main.async hack here, probably a NavigationLink bug
To track items when they get removed you can use onChange modifier, this will be called each time passed value is not the same as in previous render
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
Not sure what's with #AppStorage("notes"), it shouldn't work because this annotation only applied to simple types. If you wanna store your items in user defaults you had to do it by hand.
After removing it, you were missing #Published, that's why it wasn't updating in my case. If AppStorage could work, it may work without #Published
final class DataModel: ObservableObject {
#Published
public var notes: [NoteItem] = [
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
]
}
I have a list which normally gets formatted nicely. When I update the array items and re-add the the element it introduces a big space in the list, adding additional height to some rows.
Here is the list:
Click the add button and the new item is added with additional height:
The strange thing is that if you play "track and field" with the add button and hit it many times quickly everything works OK.
Here is some code that demonstrates the problem:
import SwiftUI
var newIndex = 0
var newItems = ["Echo", "Tango", "Hotel", "Alpha", "November", "Kilo", "Yankee", "Oscar", "Uniform", "Indigo", "Romeo", "Whisky", "Lima", "Victor", "Charlie", "Foxtrot", "Delta", "Xray", "Papa", "Juliet", "Bravo", "Golf", "Quebec", "Mike", "Sierra", "Zulu"]
struct ContentView: View
{
#State var mainList:[String] = ["Echo", "Tango", "Hotel", "Alpha", "November", "Kilo", "Yankee", "Oscar", "Uniform", "Indigo", "Romeo", "Whisky", "Lima", "Victor", "Charlie", "Foxtrot", "Delta", "Xray", "Papa", "Juliet", "Bravo", "Golf", "Quebec", "Mike", "Sierra", "Zulu"].sorted()
#State var selectedItem:String? = "Echo"
var body: some View
{
NavigationView
{
List()
{
ForEach(mainList, id: \.self)
{item in
NavigationLink(destination: DetailView(item: item), tag: item, selection: $selectedItem, label:
{
Text("\(item)")
})
}
}
}
.toolbar
{
Button(action: {addNewItem()})
{
Label("Select", systemImage: "square.and.pencil")
}
}
}
func addNewItem()
{
if newIndex == newItems.count
{
newIndex = 0
}
let item = newItems[newIndex]
if mainList.contains(item)
{
mainList = mainList.filter({ $0 != item })
}
mainList.append(item)
selectedItem = item
newIndex += 1
}
}
struct DetailView: View
{
#State var item: String
var body: some View
{
Text(item)
}
}
I have found that in this case it is caused by the filter command, but I have seen this behaviour in many of my lists across my apps. I assume it is caused by removing and adding things to the same list.
try this:
Text("\(item)").fixedSize()
and attach the .toolbar to the List not the NavigationView.
It's quite a bug in Xcode Beta 1, 2 / macOS 12 Beta 1. The thing is, you use String itself as ID, which means if two strings are the same, they'll be treated as one item in List:
ForEach(mainList, id: \.self)
This may be what you want. However, in function addNewItem(), there comes:
mainList = mainList.filter({ $0 != item })
// ...
mainList.append(item)
A same item can be removed and appended. So the List will re-calculate the height, and move the same item from where it was to the bottom, which causes a bug.
One not perfect solution is that you can wrap the item in a struct which provides an explicit ID, like this:
struct Item: Identifiable {
let id = UUID()
let value: String
// Other properties in the future...
}
And each time you want to add a new item, a copy will be created:
items = items.filter { $0.value != item }
let newItem = Item(value: item)
items.append(newItem)
In this way, List won't see the old and the new as the same item and produce moving animation. Full code:
struct Item: Identifiable {
let id = UUID()
let value: String
// Other properties in the future...
}
var nextItemIndex = 0
let itemsRow = ["Echo", "Tango", "Hotel", "Alpha", "November", "Kilo", "Yankee", "Oscar", "Uniform", "Indigo", "Romeo", "Whisky", "Lima", "Victor", "Charlie", "Foxtrot", "Delta", "Xray", "Papa", "Juliet", "Bravo", "Golf", "Quebec", "Mike", "Sierra", "Zulu"]
struct ContentView: View {
#State private var items: [Item] = itemsRow.sorted().map { Item(value: $0) }
#State private var selected: UUID?
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: DetailView(value: item.value),
tag: item.id, selection: $selected) {
Text(item.value)
}
}
}
}
.toolbar {
Button(action: addNewItem) {
Label("Select", systemImage: "square.and.pencil")
}
}
}
}
extension ContentView {
private func addNewItem() {
let item = itemsRow[nextItemIndex]
nextItemIndex = (nextItemIndex + 1) % itemsRow.count
items = items.filter { $0.value != item }
let newItem = Item(value: item)
items.append(newItem)
selected = newItem.id
}
}
Let's see if it'll be fixed in the next macOS Beta.
UPDATE: It's fixed on macOS 12 Beta 3. Now you don't have to make a copy.
I am working on an image editing app for macOS in SwiftUI, but I feel like I have a lot of code duplication, where things should probably more elegant.
I have some sliders, and some bindings to make sure the values update and a processing method is called when the slider value has changed. Currently I have a binding for each slider:
let vStretch = Binding<Double>(
get: {
self.verticalStretchLevel
},
set: {
self.verticalStretchLevel = $0
applyProcessing("vertical stretch")
}
)
let straighten = Binding<Double>(
get: {
self.straightenLevel
},
set: {
self.straightenLevel = $0
applyProcessing("straighten")
}
)
let vignette = Binding<Double>(
get: {
self.vignetteLevel
},
set: {
self.vignetteLevel = $0
applyProcessing("vignette")
}
)
This is ugly right? Can anyone point me to some article, site or give me some advice on how to make this right?
Thanks in advance!
I ended up making a view for a slider, which also has the binding:
//
// SliderView.swift
//
// Created by Michel Storms on 07/12/2020.
//
import SwiftUI
struct SliderView: View {
var runFilters: () -> Void // links to function from parent view
let label: String
let level: Binding<Double>
var body: some View {
if label.count == 1 {
HStack {
Text(label).frame(width: sliderValueWidth)
Slider(value: intensity(for: level) )
TextField("", value: level, formatter: sliderFormatter(), onCommit: { self.runFilters() } ).frame(width: sliderValueWidth)
}
.onLongPressGesture{ level.wrappedValue = 0.5 ; self.runFilters() }
.onTapGesture(count: 2, perform: { level.wrappedValue = 0.5 ; self.runFilters() })
.frame(height: sliderTextSize)
.font(.system(size: sliderTextSize))
} else {
VStack {
HStack{
Text(label)
Spacer()
TextField("", value: level, formatter: sliderFormatter(), onCommit: { self.runFilters() } ).frame(width: sliderValueWidth)
}
.frame(height: sliderTextSize)
.font(.system(size: sliderTextSize))
Slider(value: intensity(for: level) ).frame(height: sliderTextSize)
}
.onLongPressGesture{ level.wrappedValue = 0.5 ; self.runFilters() }
.onTapGesture(count: 2, perform: { level.wrappedValue = 0.5 ; self.runFilters() })
.frame(height: sliderHeight)
.font(.system(size: sliderTextSize))
}
}
func intensity(for sliderLevel: Binding<Double>) -> Binding<Double> {
Binding<Double>(
get: { sliderLevel.wrappedValue },
set: { sliderLevel.wrappedValue = $0; self.runFilters() }
)
}
func sliderFormatter() -> NumberFormatter {
let formatter = NumberFormatter()
formatter.allowsFloats = true
formatter.numberStyle = .decimal
formatter.alwaysShowsDecimalSeparator = true
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
formatter.decimalSeparator = "."
return formatter
}
}
... and then display the sliders like this:
var body: some View {
return List {
VStack {
SliderView(runFilters: self.runFilters, label: "Exposure", level: $appState.exposureLevel)
SliderView(runFilters: self.runFilters, label: "Contrast", level: $appState.contrastLevel)
SliderView(runFilters: self.runFilters, label: "Brightness", level: $appState.brightnessLevel)
SliderView(runFilters: self.runFilters, label: "Shadows", level: $appState.shadowsLevel)
SliderView(runFilters: self.runFilters, label: "Highlights", level: $appState.highlightsLevel)
SliderView(runFilters: self.runFilters, label: "Vibrance", level: $appState.vibranceLevel)
SliderView(runFilters: self.runFilters, label: "Saturation", level: $appState.saturationLevel)
SliderView(runFilters: self.runFilters, label: "Clarity", level: $appState.clarityLevel)
SliderView(runFilters: self.runFilters, label: "Black Point", level: $appState.blackpointLevel)
if debug {
SliderView(runFilters: self.runFilters, label: "DEBUG / TEST", level: $appState.debugAndTestSliderLevel)
}
}
.font(.system(size: sliderTextSize))
When I try to put 2 pikers with a different number of rows on-screen with different observers.
if I select in one number of the row that not exists in the second,
when I move to the second picker app crash with this message:
"Fatal error: Index out of range"
public enum kTrackType {
case audio
case text
}
class kTrack: NSObject, Identifiable {
public var id = UUID()
public var trakcId: String
public var title: String
public var type: kTrackType
public init(id: String, title: String, type: kTrackType) {
self.trakcId = id
self.title = title
self.type = type
}
}
and this is the main struct:
struct SelectedAudioAndSubtileView: View {
let geometry: GeometryProxy
#State var subtitlesList = [kTrack(id: "t0", title: "None", type: kTrackType.text),
kTrack(id: "t1", title: "En", type: kTrackType.text),
kTrack(id: "t2", title: "Rus", type: kTrackType.text),
kTrack(id: "t3", title: "Spn", type: kTrackType.text)]
#State var multiAudioList = [kTrack(id: "s0", title: "En", type: kTrackType.audio),
kTrack(id: "s1", title: "Rus", type: kTrackType.audio)]
#Binding var showSubtitlesPicker: Bool
#State private var selectedAudioPicker: Int = 0
#State private var selectedSubtitlePicker: Int = 0
#State private var selectedType = 0
var body: some View {
VStack {
Picker(selection: $selectedType, label: EmptyView()) {
Text("Audio").tag(0)
Text("Subtitle").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
Text(self.selectedType == 0 ? "Select Audio" : "Select Subtitle")
Divider()
if selectedType == 0 {
Picker(selection: self.$selectedAudioPicker, label: Text("")) {
ForEach(self.multiAudioList, id: \.id){ name in
Text(name.title)
}
}
} else {
Picker(selection: self.$selectedSubtitlePicker, label: Text("")) {
ForEach(self.subtitlesList, id: \.id){ name in
Text(name.title)
}
}
}
Divider()
}
.background(Color(#colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)))
.offset(y: geometry.size.height - 330)
}
After recheck, the crash happened also if you have same rows in 2 pickers!
Here is the situation :
a) the selectedValue should match the tag value, therefore in ForEach, it's better to use index not the id so that you can add tag for each items.
b) the ForEach structure is a complex one and usually to be reused for performance. So in order to force it refresh, id() modifier can be added to extra ForEach structures. There must be one ForEach without id which provides the real underlying data layer.
if selectedType == 0 {
Picker (selection: self.$selectedAudioPicker, label: Text("")) {
ForEach(0..<self.multiAudioList.count){ index in
Text(self.multiAudioList[index].title).tag(index)
}
}
} else if selectedType == 1 {
Picker(selection: self.$selectedSubtitlePicker, label: Text("")) {
ForEach(0..<self.subtitlesList.count){ index in
Text(self.subtitlesList[index].title).tag(index)
}.id(0)
}
}
I'm working with Xamarin UI Test (1.0.0). I'm trying to write an extension method that will update the selected date.
Here is what I wrote so far. It works with iOS and Android 4.x. it does not work with Android 5.x.
public static void EnterDate(this IApp app, string datePickerStyleId, DateTime date)
{
var array = app.Query(datePickerStyleId);
if (array.Length == 0)
return;
foreach (var picker in array)
{
var newMonth = date.ToString("MMM");
var newDay = date.Day.ToString();
var newYear = date.Year.ToString();
if (app.IsAndroid())
{
app.Tap(picker.Label);
//if device OS > 5 try scrolll up
app.Repl();
app.Screenshot("Date Picker");
app.Tap("month");
app.EnterText(newMonth);
app.PressEnter();
app.Tap("day");
app.EnterText(newDay);
app.PressEnter();
app.Tap("year");
app.EnterText(newYear);
app.PressEnter();
app.ClickDone();
app.Screenshot("Page After Changing Date");
app.ClickButton("Ok");
}
else if (app.IsIOS())
{
app.Tap(picker.Id);
app.Screenshot("Date Picker");
var currentDate = picker.Text;
var split = currentDate.Split(' ');
var currentMonth = split[0];
var currentDay = split[1].Remove(split[1].Length - 1);
var currentYear = split[2];
if (!newMonth.Equals(currentMonth))
{
app.Tap(newMonth);
}
if (!newDay.Equals(currentDay))
{
app.Tap(newDay);
}
if (!newYear.Equals(currentYear))
{
app.Tap(newYear);
}
app.ClickButton("Done");
}
}
}
The problem that I'm running into is that I can't figure out how to select a certain date. App.ScrollUp()/.ScrollDown() do work to navigate to different months. Although, I haven't found to determine the current month and then pick a day. Here is the output from the tree command of the Repl() tool.
tree [[object CalabashRootView] > ... > FrameLayout] [FrameLayout] id: "content" [LinearLayout] id: "parentPanel" [FrameLayout] id: "customPanel" [FrameLayout] id: "custom" [DatePicker > LinearLayout] id: "datePicker" [LinearLayout] id: "day_picker_selector_layout"
[TextView] id: "date_picker_header" text: "Thursday"
[LinearLayout] id: "date_picker_month_day_year_layout"
[LinearLayout] id: "date_picker_month_and_day_layout", label: "August 6"
[TextView] id: "date_picker_month" text: "AUG"
[TextView] id: "date_picker_day" text: "6"
[TextView] id: "date_picker_year" text: "2015"
[AccessibleDateAnimator > ... > SimpleMonthView] id: "animator", label: "Month grid of days: August 6"
[LinearLayout] id: "buttonPanel"
[Button] id: "button2" text: "Cancel"
[Button] id: "button1" text: "OK"
Does anybody know or have a method to select the date for Xamarin UI test that will work on iOS and Android (4.x and 5.x)?