Using TimelineView to use in a custom view modifier - swiftui

I am looking to use the Timelineview to automatically update the background colour for a popup sheet that is displaying a list.
I am trying to figure out how to only call it once so I can use it for multiple sections in one list and in different views that display the type of data but with different information.
I have custom colours that I would like to use and call them from the assets catalogue.
A Sample Code:
List {
Section(header: Text("Ear")
.modifier(SectionHeaderModifer())) {
ForEach(Ear.indices, id: \.self) {index in
VStack {
VStack() {
Spacer()
Text(Ear[index].name)
.modifier(NameModifier())
}
HStack {
Text(String(format: "$%.2f", Ear[index].value))
.modifier(ValueModifier())
Text(Ear[index].code)
.modifier(CodeModifier())
}
.frame(width:270)
}
.modifier(RowFormatModifier())
.listRowBackground(Color("\(hour)").opacity(0.9))
.listRowSeparator(.hidden)
}
}
Section(header: Text("Nose")
.font(.title)
.fontWeight(.thin)) {
ForEach(Nose.indices, id: \.self) {index in
VStack {
VStack() {
Spacer()
Text(Nose[index].name)
.modifier(NameModifier())
}
HStack {
Text(String(format: "$%.2f", Nose[index].value))
.modifier(ValueModifier())
Text(Nose[index].code)
.modifier(CodeModifier())
}
.frame(width:270)
}
.frame(width: 330, height:95, alignment: .center)
.modifier(RowFormatModifier())
.listRowBackground(Color("18").opacity(0.9))
.listRowSeparator(.hidden)
}
}
The second section is using the Color("18") which is in reference to 1800 just for reference as well. I have custom colour for each hour and thus why I am using the Timeline view to update the variable
The hour variable would be updated by the Timelineview as:
TimelineView(.animation) {context in
let now = Date()
let hour = Calendar.current.component(.hour, from: now)
}
I am not sure and I have tried and failed to try to put this in a ViewModifier where I could update the list row colour.
struct ChangeRowColor: ViewModifier {
func body(content: Content) -> some View {
content
TimelineView(.animation) {context in
let now = Date()
let hour = Calendar.current.component(.hour, from: now)
}
.listRowBackground(Color("\(hour)").opacity(0.9))
}
I know this is wrong because it can't see the hour variable.
Any suggestions? Where should I call the timeline to determine the hour? I only want to do it once and pass this into the different sections and other views that are of similar format.
Thanks!

Related

SwiftUI Scrollable Charts in IOS16

Using the new SwiftUI Charts framework, we can make a chart bigger than the visible screen and place it into a ScrollView to make it scrollable. Something like this:
var body : some View {
GeometryReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
Chart {
ForEach(data) { entry in
// ...
}
}
.frame(width: proxy.size.width * 2)
}
}
}
Does anybody know if it is possible to programmatically move the scroll to display a certain area of the chart?
I've tried using ScrollViewReader, setting the IDs at the x-axis labels, and trying to use the scrollTo function to navigate to any of those positions with no luck:
Chart {
/// ...
}
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { value in
if let date : Date = value.as(Date.self) {
Text(date, style: .date)
.font(.footnote)
}
}
}
This cheesy workaround seems to do the trick. I put the chart in a ZStack with an HStack overlaying the chart. The HStack contains a bunch of invisible objects that conform to the Identifiable protocol. The quantity, ids, and positions of the invisible objects match the charted data.
Since the ZStack view now contains identifiable elements, ScrollViewReader works as expected.
import SwiftUI
import Charts
struct ChartData: Identifiable {
var day: Int
var value: Int
var id: String { "\(day)" }
}
struct ContentView: View {
#State var chartData = [ChartData]()
#State var scrollSpot = ""
let items = 200
let itemWidth: CGFloat = 30
var body: some View {
VStack {
ScrollViewReader { scrollPosition in
ScrollView(.horizontal) {
// Create a ZStack with an HStack overlaying the chart.
// The HStack consists of invisible items that conform to the
// identifible protocol to provide positions for programmatic
// scrolling to the named location.
ZStack {
// Create an invisible rectangle for each x axis data point
// in the chart.
HStack(spacing: 0) {
ForEach(chartData) { item in
Rectangle()
.fill(.clear)
// Setting maxWidth to .infinity here, combined
// with spacing:0 above, makes the rectangles
// expand to fill the frame specified for the
// chart below.
.frame(maxWidth: .infinity, maxHeight: 0)
// Here, set the rectangle's id to match the
// charted data.
.id(item.id)
}
}
Chart(chartData) {
BarMark(x: .value("Day", $0.day),
y: .value("Amount", $0.value),
width: 20)
}
.frame(width: CGFloat(items) * itemWidth, height: 300)
}
}
.padding()
.onChange(of: scrollSpot, perform: {x in
if (!x.isEmpty) {
scrollPosition.scrollTo(x)
scrollSpot = ""
}
})
}
.onAppear(perform: populateChart)
Button("Scroll") {
if let x = chartData.last?.id {
print("Scrolling to item \(x)")
scrollSpot = x
}
}
Spacer()
}
}
func populateChart() {
if !chartData.isEmpty { return }
for i in 0..<items {
chartData.append(ChartData(day: i, value: (i % 10) + 2))
}
}
}
IMHO this should work out of the SwiftUI box. Apple's comments for the initializer say it creates a chart composed of a series of identifiable marks. So... if the marks are identifiable, it is not a stretch to expect ScrollViewReader to work with the chart's marks.
But noooooo!
One would hope this is an oversight on Apple's part since the framework is new, and they will expose ids for chart marks in an upcoming release.

How to recreate the grid (in screenshot) in SwiftUI without breaking navigation?

I am trying to recreate a layout similar to the Reminders app. Looking at it makes me think it was built with SwiftUI. I also believe Apple mentioned so in one of the WWDC videos (can't remember which one).
This above screenshot seems to be a List, with a LazyVGrid as the first View inside the List. Tapping on each of the items in the LazyVGrid, such as Today, Scheduled, All and Flagged, navigates to the relevant screen, which means they are all NavigationLinks. Also note that the LazyVGrid has 2 columns.
And then there is another section "My Lists" which has rows which look like regular list rows in a List with style .insetGrouped. Also, every item in this Section is a NavigationItem, and thus comes with the disclosure indicator on the right as usual. Recreating this is trivial, so it has been left out from the MRE.
I am having trouble recreating the first section, which has that LazyVGrid. I faced 3 problems (as mentioned in the image), of which I have been able to solve the first one only. The other two problems remain. I want to know if this MRE can be fixed, or is my entire approach incorrect.
I am including a minimum reproducible example below.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
RemindersView()
}
}
}
struct RemindersView: View {
private var columns: [GridItem] = [GridItem(.adaptive(minimum: 150))]
private var smartLists: [SmartList] = SmartList.sampleLists
var body: some View {
NavigationView {
List {
Section(header: Text("Using LazyVGrid")) {
grid
}
Section(header: Text("Using HStack")) {
hstack
}
}
.navigationTitle("Store")
}
.preferredColorScheme(.dark)
}
private var grid: some View {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(smartLists) { smartList in
// This use of **ZStack with an EmptyView with opacity 0** is a hack being used to avoid the disclosure indicator on each item in the grid
ZStack(alignment: .leading) {
NavigationLink( destination: SmartListView(list: smartList)) {
EmptyView()
}
.opacity(0)
SmartListView(list: smartList)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
private var hstack: some View {
ScrollView(.horizontal) {
HStack {
ForEach(smartLists) { smartList in
NavigationLink(destination: SmartListView(list: smartList)) {
SmartListView(list: smartList)
}
.buttonStyle(.plain)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
}
struct RemindersView_Previews: PreviewProvider {
static var previews: some View {
RemindersView()
}
}
struct SmartList: Identifiable {
var id: UUID = UUID()
var title: String
var count: Int
var icon: String
var iconColor: Color
static var sampleLists: [SmartList] {
let today = SmartList(title: "Today", count: 5, icon: "20.circle.fill", iconColor: .blue)
let scheduled = SmartList(title: "Scheduled", count: 12, icon: "calendar.circle.fill", iconColor: .red)
let all = SmartList(title: "All", count: 77, icon: "tray.circle.fill", iconColor: .gray)
let flagged = SmartList(title: "Flagged", count: 5, icon: "flag.circle.fill", iconColor: .orange)
return [today, scheduled, all, flagged]
}
}
struct SmartListView: View {
var list: SmartList
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .center) {
Image(systemName: list.icon)
.renderingMode(.original)
.font(.title)
.foregroundColor(list.iconColor)
Spacer()
Text("\(list.count)")
.font(.system(.title, design: .rounded))
.fontWeight(.bold)
.padding(.horizontal, 8)
}
Text(list.title)
.font(.system(.headline, design: .rounded))
.foregroundColor(.secondary)
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.gray.opacity(0.25))
)
.padding(2)
.frame(minWidth: 150)
}
}
EDIT 1: Adding video demo of what editing the dynamic Grid looks like and how the Grid has dynamic grid items (via the Edit button at the top right): https://imgur.com/a/TV0kifY

List view - any way to scroll horizontally?

I have a list that loads from a parsed CSV file built using SwiftUI and I can't seem to find a way to scroll the list horizontally.
List {
// Read each row of the array and return it as arrayRow
ForEach(arrayToUpload, id: \.self) { arrayRow in
HStack {
// Read each column of the array (Requires the count of the number of columns from the parsed CSV file - itemsInArray)
ForEach(0..<self.itemsInArray) { itemNumber in
Text(arrayRow[itemNumber])
.fixedSize()
.frame(width: 100, alignment: .leading)
}
}
}
}
.frame(minWidth: 1125, maxWidth: 1125, minHeight: 300, maxHeight: 300)
.border(Color.black)
The list renders how I would like but I'm just stuck on this one point.
Preview Image Of Layout
Swift 5;
iOS 13.4
You should use an ScrollView as Vyacheslav Pukhanov suggested but in your case the scrollView size does not get updated after the async call data arrive. So you have 2 options:
Provide a default value or an alternative view.
Provide a fixed size to the HStack inside of the ForeEach. (I used this one)
I faced the same problem laying out an horizontal grid of two columns. Here's my solution
import SwiftUI
struct ReviewGrid: View {
#ObservedObject private var reviewListViewModel: ReviewListViewModel
init(movieId: Int) {
reviewListViewModel = ReviewListViewModel(movieId: movieId)
//ReviewListViewModel will request all reviews for the given movie id
}
var body: some View {
let chunkedReviews = reviewListViewModel.reviews.chunked(into: 2)
// After the API call arrive chunkedReviews will get somethig like this => [[review1, review2],[review3, review4],[review5, review6],[review7, review8],[review9]]
return ScrollView (.horizontal) {
HStack {
ForEach(0..<chunkedReviews.count, id: \.self) { index in
VStack {
ForEach(chunkedReviews[index], id: \.id) { review in
Text("*\(review.body)*").padding().font(.title)
}
}
}
}
.frame(height: 200, alignment: .center)
.background(Color.red)
}
}
}
This is a dummy example don't expect a fancy view ;)
I hope it helps you.
You should use a horizontal ScrollView instead of the List for this purpose.
ScrollView(.horizontal) {
VStack {
ForEach(arrayToUpload, id: \.self) { arrayRow in
HStack {
ForEach(0..<self.itemsInArray) { itemNumber in
Text(arrayRow[itemNumber])
.fixedSize()
.frame(width: 100, alignment: .leading)
}
}
}
}
}

SwiftUI - NavigationLink not triggered in correct location

I am running Xcode 11 GM2 and having issues with a NavigationLink not triggering where it is expected. The app I am working on is based on Apple's sample SwiftUI app found here. The problem I am having also happens in Apple's sample.
When I add the NavigationLink to the photos:
NavigationLink(destination: PhotoDetail(photo: photo)) {
PhotoItem(photo: photo)
.frame(width: 300)
.padding(.trailing, 30)
}
The link is not accessible when you click on the image and the other content within the PhotoItem. But if you move up a bit and touch/click just below the Category name above the image, it not only triggers the link to the destination, it triggers it for all five of the items inside the scroll view.
Here is the full code from the Home page and the PhotoRow:
Home
struct HomeView: View {
// Create a categories dictionary
var categories: [String:[Photo]] {
.init(
grouping: photoData,
by: { $0.category.rawValue }
)
}
var body: some View {
NavigationView {
List(categories.keys.sorted(), id: \String.self) { key in
PhotoRow(categoryName: "\(key)".uppercased(),
photos: self.categories[key]!)
.frame(height: 320)
.padding(.top)
}
.navigationBarTitle(Text("Portfolio"))
}
}
}
PhotoRow
struct PhotoRow: View {
var categoryName: String
var photos: [Photo]
var body: some View {
VStack(alignment: .leading){
Text(self.categoryName)
.font(.title)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top) {
ForEach (self.photos) { photo in
// add navigation to detail view:
NavigationLink(destination: PhotoDetail(photo: photo)) {
PhotoItem(photo: photo)
.frame(width: 300)
.padding(.trailing, 30)
}
}
}
}
}
}
}
Any help resolving this would be appreciated.

Make Equal-Width SwiftUI Views in List Rows

I have a List that displays days in the current month. Each row in the List contains the abbreviated day, a Divider, and the day number within a VStack. The VStack is then embedded in an HStack so that I can have more text to the right of the day and number.
struct DayListItem : View {
// MARK: - Properties
let date: Date
private let weekdayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "EEE"
return formatter
}()
private let dayNumberFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "d"
return formatter
}()
var body: some View {
HStack {
VStack(alignment: .center) {
Text(weekdayFormatter.string(from: date))
.font(.caption)
.foregroundColor(.secondary)
Text(dayNumberFormatter.string(from: date))
.font(.body)
.foregroundColor(.red)
}
Divider()
}
}
}
Instances of DayListItem are used in ContentView:
struct ContentView : View {
// MARK: - Properties
private let dataProvider = CalendricalDataProvider()
private var navigationBarTitle: String {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM YYY"
return formatter.string(from: Date())
}
private var currentMonth: Month {
dataProvider.currentMonth
}
private var months: [Month] {
return dataProvider.monthsInRelativeYear
}
var body: some View {
NavigationView {
List(currentMonth.days.identified(by: \.self)) { date in
DayListItem(date: date)
}
.navigationBarTitle(Text(navigationBarTitle))
.listStyle(.grouped)
}
}
}
The result of the code is below:
It may not be obvious, but the dividers are not lined up because the width of the text can vary from row to row. What I would like to achieve is to have the views that contains the day information be the same width so that they are visually aligned.
I have tried using a GeometryReader and the frame() modifiers to set the minimum width, ideal width, and maximum width, but I need to ensure that the text can shrink and grow with Dynamic Type settings; I chose not to use a width that is a percentage of the parent because I was uncertain how to be sure that localized text would always fit within the allowed width.
How can I modify my views so that each view in the row is the same width, regardless of the width of text?
Regarding Dynamic Type, I will create a different layout to be used when that setting is changed.
I got this to work using GeometryReader and Preferences.
First, in ContentView, add this property:
#State var maxLabelWidth: CGFloat = .zero
Then, in DayListItem, add this property:
#Binding var maxLabelWidth: CGFloat
Next, in ContentView, pass self.$maxLabelWidth to each instance of DayListItem:
List(currentMonth.days.identified(by: \.self)) { date in
DayListItem(date: date, maxLabelWidth: self.$maxLabelWidth)
}
Now, create a struct called MaxWidthPreferenceKey:
struct MaxWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
let nextValue = nextValue()
guard nextValue > value else { return }
value = nextValue
}
}
This conforms to the PreferenceKey protocol, allowing you to use this struct as a key when communicating preferences between your views.
Next, create a View called DayListItemGeometry - this will be used to determine the width of the VStack in DayListItem:
struct DayListItemGeometry: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width)
}
.scaledToFill()
}
}
Then, in DayListItem, change your code to this:
HStack {
VStack(alignment: .center) {
Text(weekdayFormatter.string(from: date))
.font(.caption)
.foregroundColor(.secondary)
Text(dayNumberFormatter.string(from: date))
.font(.body)
.foregroundColor(.red)
}
.background(DayListItemGeometry())
.onPreferenceChange(MaxWidthPreferenceKey.self) {
self.maxLabelWidth = $0
}
.frame(width: self.maxLabelWidth)
Divider()
}
What I've done is I've created a GeometryReader and applied it to the background of the VStack. The geometry tells me the dimensions of the VStack which sizes itself according to the size of the text. MaxWidthPreferenceKey gets updated whenever the geometry changes, and after the reduce function inside MaxWidthPreferenceKey calculates the maximum width, I read the preference change and update self.maxLabelWidth. I then set the frame of the VStack to be .frame(width: self.maxLabelWidth), and since maxLabelWidth is binding, every DayListItem is updated when a new maxLabelWidth is calculated. Keep in mind that the order matters here. Placing the .frame modifier before .background and .onPreferenceChange will not work.
I was trying to achieve something similar. My text in one of the label in a row was varying from 2 characters to 20 characters. It messes up the horizontal alignment. I was looking to make this column in row as fixed width. And here is a very simple solution I applied to achieve that and it worked for me. Hope it can benefit someone else too.
var body: some View { // view for each row in list
VStack(){
HStack {
Text(wire.labelValueDate)
.
.
.foregroundColor(wire.labelColor)
.fixedSize(horizontal: true, vertical: false)
.frame(width: 110.0, alignment: .trailing)
}
}
}