Playing with SwiftUI and WidgetKit recently and faced a nasty problem. My widget seems to be working in the SwiftUI Canvas but it is completely empty when built onto a simulator or device.
Images:
In SwiftUI canvas:
https://github.com/beanut/images/blob/main/Screenshot%202020-12-19%20at%204.00.57%20PM.png?raw=true
When built onto the device:
https://github.com/beanut/images/blob/main/Screenshot%202020-12-19%20at%204.01.13%20PM.png?raw=true
My code:
'''
import WidgetKit
import SwiftUI
import Intents
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries = [SimpleEntry]()
let currentDate = Date()
let midnight = Calendar.current.startOfDay(for: currentDate)
let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!
//To refresh timeline every min
for offset in 0 ..< 60 * 24 {
let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: midnight)!
entries.append(SimpleEntry(date: entryDate))
}
let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
HStack() {
VStack {
Spacer()
VStack(alignment: .leading) {
Text(DateManager().getDayOfWeekInString(date: entry.date)!)
.font(Font(UIFont(name: "HoeflerText-Italic", size: 44)!))
.foregroundColor(.white)
.multilineTextAlignment(.trailing)
.opacity(1)
Text("\(DateManager().getDayAsString(date: entry.date)) \(DateManager().monthAsString(date: entry.date))")
.font(Font(UIFont(name: "Copperplate", size: 26)!))
.multilineTextAlignment(.trailing)
.opacity(1)
.foregroundColor(.gray)
}
}
.padding(.leading, 20)
.padding(.bottom, 20)
Spacer()
VStack(alignment: .trailing, spacing: -20) {
Text(DateManager().getHour(date: entry.date))
.font(Font(UIFont(name: "Copperplate-Bold", size: 86)!))
.foregroundColor(.white)
.multilineTextAlignment(.trailing)
.opacity(1)
Text(DateManager().getMinuteWithTwoDigits(date: entry.date))
.font(Font(UIFont(name: "Copperplate-Bold", size: 86)!))
.multilineTextAlignment(.trailing)
.opacity(1)
.foregroundColor(.gray)
}
.padding(.trailing, 15)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Image(uiImage: #imageLiteral(resourceName: "moon")), alignment: .center)
}
}
#main
struct Widget: SwiftUI.Widget {
let kind: String = "Widget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("TestWidget")
.description("This is an example widget.")
}
}
struct Widget_Previews: PreviewProvider {
static var previews: some View {
WidgetEntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}
'''
**Update
I noticed that the widget does show if I remove a VStack. But it doesn't show if I add the code back.
Removing the VStack:
struct WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
HStack() {
// VStack {
// Spacer()
// VStack(alignment: .leading) {
// Text(DateManager().getDayOfWeekInString(date: entry.date)!)
// .font(Font(UIFont(name: "HoeflerText-Italic", size: 44)!))
// .foregroundColor(.white)
// .multilineTextAlignment(.trailing)
// .opacity(1)
// Text("\(DateManager().getDayAsString(date: entry.date)) \(DateManager().monthAsString(date: entry.date))")
// .font(Font(UIFont(name: "Copperplate", size: 26)!))
// .multilineTextAlignment(.trailing)
// .opacity(1)
// .foregroundColor(.gray)
// }
// }
// .padding(.leading, 20)
// .padding(.bottom, 20)
Spacer()
VStack(alignment: .trailing, spacing: -20) {
Text(DateManager().getHour(date: entry.date))
.font(Font(UIFont(name: "Copperplate-Bold", size: 86)!))
.foregroundColor(.white)
.multilineTextAlignment(.trailing)
.opacity(1)
Text(DateManager().getMinuteWithTwoDigits(date: entry.date))
.font(Font(UIFont(name: "Copperplate-Bold", size: 86)!))
.multilineTextAlignment(.trailing)
.opacity(1)
.foregroundColor(.gray)
}
.padding(.trailing, 15)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Image(uiImage: #imageLiteral(resourceName: "moon")), alignment: .center)
}
}
It does show the UI elements:
https://github.com/beanut/images/blob/main/IMG_002DAA718D1C-1.jpeg?raw=true
Please help me with it and thanks in advance :)!
I stumbled upon this question trying to find a fix for this same issue for myself, and have since found a solution.
I don't believe the VStack was the problem here, but rather force unwrapping one or more of the values for the UI elements inside it, as in this line for example:
Text(DateManager().getDayOfWeekInString(date: entry.date)!)
Force unwrapping a nil value was what was stopping drawing widget's UI for all sizes for me. Try unwrapping the values the safe way or provide a fallback by nil coalescing.
This might be because you call WidgetCenter.shared.reloadAllTimelines in the getTimeline function:
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
...
for offset in 0 ..< 60 * 24 {
WidgetCenter.shared.reloadAllTimelines() // <- this is wrong
let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: midnight)!
entries.append(SimpleEntry(date: entryDate))
}
let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
completion(timeline)
}
The whole point of WidgetCenter.shared.reloadAllTimelines is to force refresh the timeline and effectively call the getTimeline function again.
It's a bad idea to call it from inside getTimeline, especially in the loop.
The code in previews may be working because the getTimeline function is called once only and all repetitive calls are ignored.
Related
I have an Quote app made in Flutter and now i create widget for IOS. My Widget loads some data from UserDefaults/ Appgroups and depending on that it shows some quotes and a picture. This works with the first start when i press button to set quote and images.
I want the widget to show new quotes every minute, tried different approaches with the Timeline, but it is not working. Can anyone help me make this works?
Below the code for the widget:
func placeholder(in context: Context) -> ExampleEntry {
ExampleEntry(date: Date(), flutterData: FlutterData( title: "Placeholder Title", message: "Placeholder Message",imageId:"tree", textColor:"Color.black"))
}
func getSnapshot(in context: Context, completion: #escaping (ExampleEntry) -> ()) {
let data = UserDefaults.init(suiteName:"***")
let entry = ExampleEntry(date: Date(),flutterData: FlutterData( title: data?.string(forKey: "title") ?? "Set quote and background in Settings > Widget", message: data?.string(forKey: "message") ?? "No Message Set", imageId: data?.string(forKey: "imageId") ?? "Color.black", textColor: data?.string(forKey: "textColor") ?? "Color.white"))
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<ExampleEntry>) -> ()) {
let data = UserDefaults.init(suiteName:"***")
var entries = [ExampleEntry]()
let currentDate = Date()
let entry = ExampleEntry(date: currentDate,flutterData:FlutterData(title: data?.string(forKey: "title") ?? "Set quote and background in Settings > Widget", message: data?.string(forKey: "message") ?? "No Message Set", imageId: data?.string(forKey: "imageId") ?? "Color.black", textColor: data?.string(forKey: "textColor") ?? "Color.white"))
let entryDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)
print(entryDate)
let timeline = Timeline(entries: [entry], policy: .after(entryDate!))
completion(timeline)
}
}
var body: some View {
VStack.init(alignment: .leading, spacing: nil, content: {
Text(entry.flutterData!.title)
.font(.body)
.foregroundColor(Color.white)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: Alignment.center)
.padding(.vertical)
.padding(.horizontal,6)
.background(
Image(entry.flutterData!.imageId)
.resizable()
.scaledToFill()
.frame( maxWidth: .infinity, maxHeight: .infinity)
)
.onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)) { _ in
// make sure you don't call this too often
WidgetCenter.shared.reloadAllTimelines()
}
}
)
}
}
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind,provider: Provider()) { entry in
HomeWidgetExampleEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
I was trying to create the UI similar to this, went through the few articles and found that overlay should be used to create the same UI, However everything is fine unless List or any iterating view is used, when the overlay crosses the other sections frame it doesn't seem to be behaving as expected?
List { // 3 elements
HStack(){
DropdownSelector(
placeholder: "Day of the week",
options: options,
onOptionSelected: { option in
print(option)
})
.padding(.horizontal)
.zIndex(1)
DropdownSelector(
placeholder: "Day of the week",
options: options,
onOptionSelected: { option in
print(option)
})
.padding(.horizontal)
.zIndex(1)
}.zIndex(1)
}
Any suggestion ?
You need to update .zIndex(1) value with a state variable like in the code below
import SwiftUI
struct DropdownOption: Hashable {
let key: String
let value: String
public static func == (lhs: DropdownOption, rhs: DropdownOption) -> Bool {
return lhs.key == rhs.key
}
}
struct DropdownRow: View {
var option: DropdownOption
var onOptionSelected: ((_ option: DropdownOption) -> Void)?
var body: some View {
Button(action: {
if let onOptionSelected = self.onOptionSelected {
onOptionSelected(self.option)
}
}) {
HStack {
Text(self.option.value)
.font(.system(size: 14))
.foregroundColor(Color.black)
Spacer()
}
}
.padding(.horizontal, 16)
.padding(.vertical, 5)
}
}
struct Dropdown: View {
var options: [DropdownOption]
var onOptionSelected: ((_ option: DropdownOption) -> Void)?
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(self.options, id: \.self) { option in
DropdownRow(option: option, onOptionSelected: self.onOptionSelected)
}
}
}
.frame(minHeight: CGFloat(options.count) * 30, maxHeight: 250)
.padding(.vertical, 5)
.background(Color.white)
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 1)
)
}
}
struct DropdownSelector: View {
#State private var shouldShowDropdown = false
#State private var selectedOption: DropdownOption? = nil
var placeholder: String
var options: [DropdownOption]
var onOptionSelected: ((_ option: DropdownOption) -> Void)?
var onDropdownSelected: (() -> Void)
private let buttonHeight: CGFloat = 45
var body: some View {
Button(action: {
self.shouldShowDropdown.toggle()
onDropdownSelected()
}) {
HStack {
Text(selectedOption == nil ? placeholder : selectedOption!.value)
.font(.system(size: 14))
.foregroundColor(selectedOption == nil ? Color.gray: Color.black)
Spacer()
Image(systemName: self.shouldShowDropdown ? "arrowtriangle.up.fill" : "arrowtriangle.down.fill")
.resizable()
.frame(width: 9, height: 5)
.font(Font.system(size: 9, weight: .medium))
.foregroundColor(Color.black)
}
}
.padding(.horizontal)
.cornerRadius(5)
.frame(width: .infinity, height: self.buttonHeight)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 1)
)
.overlay(
VStack {
Image("top-image")
.resizable()
.aspectRatio(contentMode: .fit)
.scaledToFit()
if self.shouldShowDropdown {
Spacer(minLength: buttonHeight + 10)
Dropdown(options: self.options, onOptionSelected: { option in
shouldShowDropdown = false
selectedOption = option
self.onOptionSelected?(option)
})
}
}, alignment: .topLeading
)
.background(
RoundedRectangle(cornerRadius: 5).fill(Color.white)
)
}
}
struct CellStatus {
var zIndex: Double
var isOpen: Bool
}
struct DropdownSelectorView: View {
#State private var address: String = ""
#State private var indexToBringForward = 0
#State private var eachCellStatus: [CellStatus] = Array(repeating: CellStatus(zIndex: 0, isOpen: false), count: 6)
static var uniqueKey: String {
UUID().uuidString
}
let options: [DropdownOption] = [
DropdownOption(key: uniqueKey, value: "Sunday"),
DropdownOption(key: uniqueKey, value: "Monday"),
DropdownOption(key: uniqueKey, value: "Tuesday"),
DropdownOption(key: uniqueKey, value: "Wednesday"),
DropdownOption(key: uniqueKey, value: "Thursday"),
DropdownOption(key: uniqueKey, value: "Friday"),
DropdownOption(key: uniqueKey, value: "Saturday")
]
private var gridItemLayout = [GridItem(.adaptive(minimum: 200))]
var body: some View {
VStack(spacing: 20) {
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0..<6), id: \.self) { index in
DropdownSelector(
placeholder: "Day of the week",
options: options,
onOptionSelected: { option in
print(option)
},
onDropdownSelected: {
eachCellStatus[index].isOpen.toggle()
if ( eachCellStatus[index].isOpen ){
let cellWithHighestZIndex = eachCellStatus.max(by: {$0.zIndex < $1.zIndex})
eachCellStatus[index].zIndex = (cellWithHighestZIndex?.zIndex ?? 0) + 1
}
}
)
.padding(.horizontal)
.zIndex(eachCellStatus[index].zIndex)
}
}
.zIndex(1)
Group {
TextField("Full Address", text: $address)
.font(.system(size: 14))
.padding(.horizontal)
}
.frame(width: 400, height: 45)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 1)
)
.padding(.horizontal)
}
}
}
struct DropdownSelectorView_Previews: PreviewProvider {
static var previews: some View {
DropdownSelectorView()
}
}
I'm trying to allow the FileManager to check if the matching Image Selected is saved. When saved, it needs to update the views on both the MainScreenView and BadgeScreenView. I am getting a error that "Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update." and "Result of 'BadgeScreenView' initializer is unused" after the image is selected and checked to see if it is saved at the right locations.
var ContentViewBadge = UIImage(systemName: "questionmark")!
var fileURL: URL?
func saveImage() {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
fileURL = furl
try ContentViewBadge.pngData()?.write(to: furl)
print("Image \(ContentViewBadge) is saved to \(furl)")
} catch {
print("could not create imageFile")
}
let finding = fileURL
let fileExists = FileManager().fileExists(atPath: finding!.path)
if fileExists {
#State var IsTrue: Bool = true
BadgeScreenView(TrueBadge: $IsTrue)
//Change State variable "TrueBadge" here
print("Found something!")
}
}
import SwiftUI
import Foundation
var IsDone = false
struct BadgeScreenView: View {
#Binding var TrueBadge: Bool //Need help switching this Binding to true
#State private var ComplianceBadgeIsPicking = UIImage(named: "BlankComplianceBadge")!
#State private var isShwoingPhotoPicker = false
#State private var ShowInstruction = false
#State private var AlertToReplaceBade = false
var body: some View {
//The beginning
if TrueBadge {
Color("MainBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Text("Clearance Status")
.font(.title)
.fontWeight(.semibold)
.offset(y: -15)
.foregroundColor(.white)
Text("Vaccine Compliant")
.foregroundColor(.white)
.bold()
.font(.system(size: 30))
Image(uiImage: ContentViewBadge)
.resizable()
.aspectRatio(contentMode: .fit)
.scaledToFit()
Button(action: {
AlertToReplaceBade.toggle()
}) {
Image(systemName: "trash" )
Text("Remove")
}
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(15)
.offset(y: 13)
}.alert(isPresented: $AlertToReplaceBade, content: {
Alert(title: Text("Are you sure you would like to remove your current badge?"),
message: Text("Remeber that this badge is and will be permanently removed"),
primaryButton: .default(Text("Yes"), action: {
// Somehow need to remove the image and activate the UIImagePickerController
isShwoingPhotoPicker.toggle()
}), secondaryButton: .cancel(Text("No, I do not")))
}).sheet(isPresented: $isShwoingPhotoPicker, content: {
PhotoPicker(Badge: $ComplianceBadgeIsPicking)
})
)}
else {
Color("ExpiredBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Image(systemName: "person.crop.circle.badge.questionmark.fill")
.font(.system(size:150))
.offset(y: -10)
.foregroundColor(.black)
Text("Compliance Badge")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.black)
.offset(y: -2)
Text("You do not have a current vaccine compliant badge. Please upload one that shows you are vaccine compliant or within 'green' status")
.font(.system(size: 15))
.foregroundColor(.black)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.frame(width: 270, height: 140, alignment: .center)
.offset(y: -26)
Button(action: {
ShowInstruction.toggle()
}) {
Image(systemName: "questionmark.circle")
Text("How to upload")
.bold()
.font(.system(size:20))
}
.offset(y: -40)
Button(action: {
isShwoingPhotoPicker.toggle()
}) {
Image(systemName: "square.and.arrow.up")
Text("Upload Badge")
.bold()
.font(.system(size:20))
}
.offset(y: -10)
}.sheet(isPresented: $ShowInstruction, content: {
Instruction()
})
.sheet(isPresented: $isShwoingPhotoPicker, content: {
PhotoPicker(Badge: $ComplianceBadgeIsPicking)
})
.accentColor(.black)
)
}
//The End
}
}
There's not enough for a minimal reproducible example in your code, so I had to make some guesses here, but this is the gist of what I'd expect things to look like. Note that saveImage is inside the View and thus has access to change the state.
It's not clear to me, though, where you call saveImage (you don't do it anywhere in your included code), which could effect things further.
let contentViewBadge = UIImage(systemName: "questionmark")!
struct BadgeScreenView: View {
#Binding var trueBadge: Bool
#State private var complianceBadgeIsPicking = UIImage(named: "BlankComplianceBadge")!
#State private var isShowingPhotoPicker = false
#State private var showInstruction = false
#State private var alertToReplaceBade = false
func saveImage() {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
try contentViewBadge.pngData()?.write(to: furl)
print("Image \(contentViewBadge) is saved to \(furl)")
let fileExists = FileManager().fileExists(atPath: furl.path)
if fileExists {
trueBadge = true
print("Found something!")
}
} catch {
print("could not create imageFile")
}
}
var body: some View {
//The beginning
if trueBadge {
Color("MainBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Text("Clearance Status")
.font(.title)
.fontWeight(.semibold)
.offset(y: -15)
.foregroundColor(.white)
Text("Vaccine Compliant")
.foregroundColor(.white)
.bold()
.font(.system(size: 30))
Image(uiImage: contentViewBadge)
.resizable()
.aspectRatio(contentMode: .fit)
.scaledToFit()
Button(action: {
alertToReplaceBade.toggle()
}) {
Image(systemName: "trash" )
Text("Remove")
}
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(15)
.offset(y: 13)
}.alert(isPresented: $alertToReplaceBade, content: {
Alert(title: Text("Are you sure you would like to remove your current badge?"),
message: Text("Remeber that this badge is and will be permanently removed"),
primaryButton: .default(Text("Yes"), action: {
// Somehow need to remove the image and activate the UIImagePickerController
isShowingPhotoPicker.toggle()
}), secondaryButton: .cancel(Text("No, I do not")))
}).sheet(isPresented: $isShowingPhotoPicker, content: {
PhotoPicker(Badge: $complianceBadgeIsPicking)
})
)}
else {
Color("ExpiredBadgeScreen")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Image(systemName: "person.crop.circle.badge.questionmark.fill")
.font(.system(size:150))
.offset(y: -10)
.foregroundColor(.black)
Text("Compliance Badge")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.black)
.offset(y: -2)
Text("You do not have a current vaccine compliant badge. Please upload one that shows you are vaccine compliant or within 'green' status")
.font(.system(size: 15))
.foregroundColor(.black)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.frame(width: 270, height: 140, alignment: .center)
.offset(y: -26)
Button(action: {
showInstruction.toggle()
}) {
Image(systemName: "questionmark.circle")
Text("How to upload")
.bold()
.font(.system(size:20))
}
.offset(y: -40)
Button(action: {
isShwoingPhotoPicker.toggle()
}) {
Image(systemName: "square.and.arrow.up")
Text("Upload Badge")
.bold()
.font(.system(size:20))
}
.offset(y: -10)
}.sheet(isPresented: $showInstruction, content: {
Instruction()
})
.sheet(isPresented: $isShowingPhotoPicker, content: {
PhotoPicker(Badge: $complianceBadgeIsPicking)
})
.accentColor(.black)
)
}
//The End
}
}
An alternative (or addition) to putting savingImage inside the view would be to explicitly pass a binding to it. The function would look like this:
func saveImage(binding: Binding<Bool>) {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
try contentViewBadge.pngData()?.write(to: furl)
print("Image \(contentViewBadge) is saved to \(furl)")
let fileExists = FileManager().fileExists(atPath: furl.path)
if fileExists {
binding.wrappedValue = true
print("Found something!")
}
} catch {
print("could not create imageFile")
}
}
And you would call it like this:
saveImage(binding: $trueBadge)
Note: I changed your variable names to fit the Swift conventions of using lowercase letters to start variable/property names
I have a widget view looking like this:
struct WidgetEntryView: View {
var entry: Provider.Entry
#Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
ZStack {
VStack(spacing: 12) {
// ...
}
.padding(10)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Color.red.edgesIgnoringSafeArea(.all))
case .systemMedium:
ZStack {
VStack(spacing: 12) {
// ...
}
.padding(10)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Color.blue.edgesIgnoringSafeArea(.all))
default:
ZStack {
VStack(spacing: 12) {
// ...
}
.padding(10)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Color.black.edgesIgnoringSafeArea(.all))
}
}
}
The widget supports all 3 main size families:
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("MyWidget")
.description("...")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
And here's my PreviewProvider:
struct Widget_Previews: PreviewProvider {
static var previews: some View {
Group {
WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())
.previewContext(WidgetPreviewContext(family: .systemSmall))
WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())
.previewContext(WidgetPreviewContext(family: .systemMedium))
WidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent())
.previewContext(WidgetPreviewContext(family: .systemLarge))
}
}
}
So I have a preview for each size family on the canvas, but for some reason, all of them are rendered with a blue background. Or in other words, all of them are rendered as a .systemMedium family. When I actually run the widget on the simulator, it has the correct look. Why does the preview always skip to the .systemMedium case and ignores the other ones? How can I fix this?
The #Environment var did not work for previews.
But you can use an environment modifier like this:
YourWidgetView()
.previewContext(WidgetPreviewContext(family: .systemSmall))
.environment(\.widgetFamily, .systemSmall)
But you have to write an EnvironmentKey extension. This is the solution i used:
How to set widgetFamily environment
I built a simple horizontal calendar with SwiftUI and it works fine. But I want that it automatically scroll to the current date at app launch. How can I do this? I assume I should use GeometryReader to get a position of frame with current date and set an offset based on it but ScrollView doesn't have a content offset modifier. I know that in iOS 14 we now have ScrollViewReader but what about iOS 13?
struct MainCalendar: View {
#Environment(\.calendar) var calendar
#State private var month = Date()
#State private var selectedDate: Date = Date()
var body: some View {
VStack(spacing: 30) {
MonthAndYearView(date: $month)
WeekDaysView(date: month)
}
.padding(.vertical)
}
}
struct MonthAndYearView: View {
#Environment(\.calendar) var calendar
private let formatter = DateFormatter.monthAndYear
#Binding var date: Date
var body: some View {
HStack(spacing: 20) {
Button(action: {
self.date = calendar.date(byAdding: .month, value: -1, to: date)!
}, label: {
Image(systemName: "chevron.left")
})
Text(formatter.string(from: date))
.font(.system(size: 18, weight: .semibold))
Button(action: {
self.date = calendar.date(byAdding: .month, value: 1, to: date)!
}, label: {
Image(systemName: "chevron.right")
})
}
}
}
struct WeekDaysView: View {
#Environment(\.calendar) var calenar
let date: Date
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(days, id: \.self) { day in
VStack(spacing: 20) {
Text(daySymbol(date: day))
.font(.system(size: 14, weight: .regular))
if calenar.isDateInToday(day) {
Text("\(dayNumber(date: day))")
.foregroundColor(Color.white)
.background(Circle().foregroundColor(.blue)
.frame(width: 40, height: 40))
} else {
Text("\(dayNumber(date: day))")
}
}
}
}
.padding([.horizontal])
.padding(.bottom, 15)
}
}
private func dayNumber(date: Date) -> String {
let formatter = DateFormatter.dayNumber
let dayNumber = formatter.string(from: date)
return dayNumber
}
private var days: [Date] {
guard let interval = calenar.dateInterval(of: .month, for: date) else { return [] }
return calenar.generateDates(inside: interval, matching: DateComponents(hour: 0, minute: 0, second: 0))
}
private func daySymbol(date: Date) -> String {
let dayFormatter = DateFormatter.weekDay
let weekDay = dayFormatter.string(from: date)
return weekDay
}
}
This library https://github.com/Amzd/ScrollViewProxy is solved my problem.
struct WeekDaysView: View {
let date: Date
#Environment(\.calendar) var calendar
#State private var scrollTarget: Int? = nil
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(Array(zip(days.indices, days)), id: \.0) { index, day in
VStack(spacing: 20) {
Text(daySymbol(date: day))
.font(.system(size: 14, weight: .regular))
.scrollId(index)
if calendar.isDateInToday(day) {
Text("\(dayNumber(date: day))")
.foregroundColor(Color.white)
.background(Circle().foregroundColor(.blue)
.frame(width: 40, height: 40))
.onAppear {
scrollTarget = index
}
} else {
Text("\(dayNumber(date: day))")
}
}
}
}
.padding([.horizontal])
.padding(.bottom, 15)
}
.onAppear {
DispatchQueue.main.async {
withAnimation {
proxy.scrollTo(scrollTarget, alignment: .center, animated: true)
}
}
}
}
}