I would like to be able to pass the GaugeStyle for a Gauge as a parameter to my own SwiftUI view, however I can't seem to get this to work with the compiler. When I try the below code I get an error, Type 'any View' cannot conform to 'View'.
import SwiftUI
struct MyGauge: View {
let min = 0.0
let max = 10.0
let current = 5.0
let style: any GaugeStyle = .accessoryCircular
var body: some View {
Gauge(value: current, in: min...max) {
Text("Gauge")
} currentValueLabel: {
Text("\(current)")
} minimumValueLabel: {
Text("\(min)")
} maximumValueLabel: {
Text("\(max)")
}
.gaugeStyle(style)
}
}
(Code is simplified for brevity.)
What's the correct way to allow a GaugeStyle to be passed around?
SwiftUI requires concrete types for most areas of a View especially wrappers and view modifiers.
The any keyword is "existential"
#available(iOS 16.0, *)
struct MyGauge<G>: View where G : GaugeStyle{
let min = 0.0
let max = 10.0
let current = 5.0
let style: G
init(style: G = .accessoryCircular){
self.style = style
}
var body: some View {
Gauge(value: current, in: min...max) {
Text("Gauge")
} currentValueLabel: {
Text("\(current)")
} minimumValueLabel: {
Text("\(min)")
} maximumValueLabel: {
Text("\(max)")
}
.gaugeStyle(style)
}
}
#available(iOS 16.0, *)
struct MyGauge_Previews: PreviewProvider {
static var previews: some View {
MyGauge()
MyGauge(style: .accessoryLinear)
}
}
Related
As far as I know, you can't use if let on variables that aren't optional...
func run() {
let title = "Hello, world!"
if let title = title { /// Initializer for conditional binding must have Optional type, not 'String'
print(title)
}
}
... but for some reason, when it's inside a closure, it works:
func run() {
let title = "Hello, world!"
let closure = {
if let title = title { /// No error!
print(title)
}
}
closure()
}
This was on Xcode 14.0 beta (14A5228q). Is this a bug, or a feature? When I tested on Xcode 13.3 (13E113), the error appeared again.
On a side note, the following SwiftUI code compiles fine on both Xcode 13 and Xcode 14:
struct ContentView: View {
let title = "Hello, world!"
var body: some View {
VStack {
if let title = title { /// Works fine!
Text(title)
}
}
}
}
And it's not just VStack — anything marked #ViewBuilder seems to work.
struct ContentView: View {
let title = "Hello, world!"
var body: some View {
ContainerView {
if let title = title { /// works fine!
Text(title)
}
}
}
}
struct ContainerView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
}
}
Is this a documented feature, or a bug?
I'm trying to build a calendar app for iOS using KVKCalendar but it's not originally build for Swift UI so I'm kind of struggling to achieve what I want to do.
My goal is changing calendar type (such as daily, weekly, monthly) by a segment controller, exactly like this example (which is already provided in git repository).
sampleUI
So far, I managed to display daily style calendar view as default. But I don't know how I can change its calendar type from ContentView.swift
Does anyone know about this type of ViewController ↔ Swift UI thing?
My code
My ContentView.swift is like this
import SwiftUI
import KVKCalendar
struct ContentView: View {
#State var events: [Event] = []
#State var selectedType: CalendarType = .week // I want this to change its calendar type.
var body: some View {
VStack{
CalendarDisplayView(events: $events)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And my ContentDisplayView.swift is like this
import SwiftUI
import EventKit
import KVKCalendar
struct CalendarDisplayView: UIViewRepresentable {
#Binding var events: [Event]
public init(events: Binding<[Event]>) {
self._events = events
}
private var calendar: CalendarView = {
var style = Style()
if UIDevice.current.userInterfaceIdiom == .phone {
style.timeline.widthTime = 40
style.timeline.currentLineHourWidth = 45
style.timeline.offsetTimeX = 2
style.timeline.offsetLineLeft = 2
style.headerScroll.titleDateAlignment = .center
style.headerScroll.isAnimateTitleDate = true
style.headerScroll.heightHeaderWeek = 70
style.event.isEnableVisualSelect = false
style.month.isHiddenTitle = true
style.month.weekDayAlignment = .center
} else {
style.timeline.widthEventViewer = 350
style.headerScroll.fontNameDay = .systemFont(ofSize: 17)
}
style.month.autoSelectionDateWhenScrolling = true
style.timeline.offsetTimeY = 25
style.startWeekDay = .monday
style.timeSystem = .current ?? .twelve
style.systemCalendars = ["Calendar"]
if #available(iOS 13.0, *) {
style.event.iconFile = UIImage(systemName: "paperclip")
}
style.locale = Locale.current
style.timezone = TimeZone.current
return CalendarView(frame: UIScreen.main.bounds, style: style)
}()
func makeUIView(context: UIViewRepresentableContext<CalendarDisplayView>) -> CalendarView {
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
return calendar
}
func updateUIView(_ uiView: CalendarView, context: UIViewRepresentableContext<CalendarDisplayView>) {
context.coordinator.events = events
}
func makeCoordinator() -> CalendarDisplayView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, CalendarDataSource, CalendarDelegate {
private let view: CalendarDisplayView
var events: [Event] = [] {
didSet {
view.calendar.reloadData()
}
}
init(_ view: CalendarDisplayView) {
self.view = view
super.init()
}
func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] {
return events
}
}
}
First, add a #Binding inside CalendarDisplayView so it can update:
#Binding var selectedType: CalendarType
Then, pass in ContentView's $selectedType for CalendarDisplayView's selectedType, just like how you passed in $events.
/// here!
CalendarDisplayView(events: $events, selectedType: $selectedType)
Finally, update the type inside updateUIView, which is called whenever the #Binding changes.
func updateUIView(_ uiView: CalendarView, context: UIViewRepresentableContext<CalendarDisplayView>) {
context.coordinator.events = events
calendar.set(type: selectedType, date: Date()) /// I've never used this library, so you might need to replace `Date()` with something else
calendar.reloadData()
}
This question is similar to this unanswered question from the Apple Developer Forums, but with a slightly different scenario:
I have a view with a #FetchRequest property of type FetchedResults<Metric>
Within the view, I display the list of those objects
I can tap on one item to select it, storing that selection in a #State var selection: Metric? = nil property.
Here's the properties I defined for my #FocusedValue:
struct FocusedMetricValue: FocusedValueKey {
typealias Value = Metric?
}
extension FocusedValues {
var metricValue: FocusedMetricValue.Value? {
get { self[FocusedMetricValue.self] }
set { self[FocusedMetricValue.self] = newValue }
}
}
Here's how I set the focusedValue from my list view:
.focusedValue(\.metricValue, selection)
And here's how I'm using the #FocusedValue on my Commands struct:
struct MacOSCommands: Commands {
#FocusedValue(\.metricValue) var metric
var body: some Commands {
CommandMenu("Metric") {
Button("Test") {
print(metric??.name ?? "-")
}
.disabled(metric == nil)
}
}
}
The code builds successfully, but when I run the app and select a Metric from the list, the app freezes. If I pause the program execution in Xcode, this is the stack trace I get:
So, how can I make #FocusedValue work in this scenario, with an optional object from a list?
I ran into the same issue. Below is a View extension and ViewModifier that present a version of focusedValue which accepts an Binding to an optional. Not sure why this wasn't included in the framework as it corresponds more naturally to a selection situation in which there can be none...
extension View{
func focusedValue<T>(_ keypath: WritableKeyPath<FocusedValues, Binding<T>?>, selection: Binding<T?>) -> some View{
self.modifier(FocusModifier(keypath: keypath, optionalBinding: selection))
}
}
struct FocusModifier<T>: ViewModifier{
var keypath: WritableKeyPath<FocusedValues, Binding<T>?>
var optionalBinding: Binding<T?>
func body(content: Content) -> some View{
Group{
if optionalBinding.wrappedValue == nil{
content
}
else if let binding = Binding(optionalBinding){
content
.focusedValue(keypath, binding)
}
else{
content
}
}
}
}
In your car usage would look like:
.focusedValue(\.metricValue, selection: $selection)
I have also found that the placement of this statement is finicky. I can only make things work when I place this on the NavigationView itself as opposed to one of its descendants (eg List).
// 1 CoreData optional managedObject in #State var selection
#State var selection: Metric?
// 2 modifiers on View who own the list with the selection
.focusedValue(\.metricValue, $selection)
.focusedValue(\.deleteMetricAction) {
if let metric = selection {
print("Delete \(metric.name ?? "unknown metric")?")
}
}
// 3
extension FocusedValues {
private struct SelectedMetricKey: FocusedValueKey {
typealias Value = Binding<Metric?>
}
private struct DeleteMetricActionKey: FocusedValueKey {
typealias Value = () -> Void
}
var metricValue: Binding<Metric?>? {
get { self[SelectedMetricKey.self] }
set { self[SelectedMetricKey.self] = newValue}
}
var deleteMetricAction: (() -> Void)? {
get { self[DeleteMetricActionKey.self] }
set { self[DeleteMetricActionKey.self] = newValue }
}
}
// 4 Use in macOS Monterey MenuCommands
struct MetricsCommands: Commands {
#FocusedValue(\.metricValue) var selectedMetric
#FocusedValue(\.deleteMetricAction) var deleteMetricAction
var body: some Commands {
CommandMenu("Type") {
Button { deleteMetricAction?() } label: { Text("Delete \(selectedMetric.name ?? "unknown")") }.disabled(selectedMetric?.wrappedValue == nil || deleteMetricAction == nil )
}
}
}
// 5 In macOS #main App struct
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
.commands {
SidebarCommands()
MetricsCommands()
}
}
Use of #FocusedValues in Apple WWDC21 example
Use of FocusedValues in SwiftUI with Majid page
Use of #FocusedSceneValue with example of action value passed, in Apple documentation
For SwiftUI 3 macOS Table who support multiple selections
// 1 Properties
#Environment(\.managedObjectContext) var context
var type: Type
var fetchRequest: FetchRequest<Propriete>
var proprietes: FetchedResults<Propriete> { fetchRequest.wrappedValue }
#State private var selectedProprietes = Set<Propriete.ID>()
// 2 Init from Type managedObject who own Propriete managedObjects
// #FecthRequest required to have updates in Table (when delete for example)
init(type: Type) {
self.type = type
fetchRequest = FetchRequest<Propriete>(entity: Propriete.entity(),
sortDescriptors: [ NSSortDescriptor(key: "nom",
ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) ],
predicate: NSPredicate(format: "type == %#", type))
}
// 3 Table view
VStack {
Table(proprietes, selection: $selectedProprietes) {
TableColumn("Propriétés :", value: \.wrappedNom)
}
.tableStyle(BorderedTableStyle())
.focusedSceneValue(\.selectedProprietes, $selectedProprietes)
.focusedSceneValue(\.deleteProprietesAction) {
deleteProprietes(selectedProprietes)
}
}
// 4 FocusedValues
extension FocusedValues {
private struct FocusedProprietesSelectionKey: FocusedValueKey {
typealias Value = Binding<Set<Propriete.ID>>
}
var selectedProprietes: Binding<Set<Propriete.ID>>? {
get { self[FocusedProprietesSelectionKey.self] }
set { self[FocusedProprietesSelectionKey.self] = newValue }
}
}
// 5 Delete (for example) in Table View
private func deleteProprietes(_ proprietesToDelete: Set<Propriete.ID>) {
var arrayToDelete = [Propriete]()
for (index, propriete) in proprietes.enumerated() {
if proprietesToDelete.contains(propriete.id) {
let propriete = proprietes[index]
arrayToDelete.append(propriete)
}
}
if arrayToDelete.count > 0 {
print("array to delete: \(arrayToDelete)")
for item in arrayToDelete {
context.delete(item)
print("\(item.wrappedNom) deleted!")
}
try? context.save()
}
}
How to manage selection in Table
In the attached code example I get a lot of extra top-spacing in my TextField. If I change the content to only be a single line, say "content", then it fits snugly. How can I get the same tight-fitting behaviour the single line has for a multi-line text?
Previews and code were made with Xcode 11.1 / Swift 5.1
import SwiftUI
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
var body: some View {
TextField("Custom placeholder", text: content)
.background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent\ncontent\ncontent\ncontent"
return TextFieldDemo(content: .constant(content))
.previewLayout(.sizeThatFits)
}
}
#endif
Here is the example if I change the "let content" line to
let content = "content"
It seems there's no direct argument to manage multiline padding correctly. They are maybe underdevelopping. But the following will give you a straight workaround solution to what you are expecting.
extension String{
var extraLines : String{ get{
return self + String(repeating:"\n", count: self.components(separatedBy: "\n").count - 1)
}}
}
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
#State var height : CGFloat? //current height
let constHeightRatio : CGFloat = 0.55 //use for assembly with other fonts.
let defaultHeight : CGFloat = 250 //use for assembly with other views.
var body: some View {
TextField("Custom placeholder", text: content).environment(\.multilineTextAlignment, .center).alignmentGuide(.bottom) { (ViewDimensions) -> CGFloat in
if self.height == nil {self.height = ViewDimensions.height}
return ViewDimensions.height
}.frame( height: (height ?? defaultHeight) * constHeightRatio, alignment: .bottom).background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent".extraLines
return
TextFieldDemo(content: .constant(content))
}
}
#endif
This works fine for single view. If view assembly is required (with other stacking views, etc), you may adjust defaultHeight and/or constHeightRatio to achieve what you want. Hopefully it works for you too.
In the attached code example I get a lot of extra top-spacing in my TextField. If I change the content to only be a single line, say "content", then it fits snugly. How can I get the same tight-fitting behaviour the single line has for a multi-line text?
Previews and code were made with Xcode 11.1 / Swift 5.1
import SwiftUI
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
var body: some View {
TextField("Custom placeholder", text: content)
.background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent\ncontent\ncontent\ncontent"
return TextFieldDemo(content: .constant(content))
.previewLayout(.sizeThatFits)
}
}
#endif
Here is the example if I change the "let content" line to
let content = "content"
It seems there's no direct argument to manage multiline padding correctly. They are maybe underdevelopping. But the following will give you a straight workaround solution to what you are expecting.
extension String{
var extraLines : String{ get{
return self + String(repeating:"\n", count: self.components(separatedBy: "\n").count - 1)
}}
}
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
#State var height : CGFloat? //current height
let constHeightRatio : CGFloat = 0.55 //use for assembly with other fonts.
let defaultHeight : CGFloat = 250 //use for assembly with other views.
var body: some View {
TextField("Custom placeholder", text: content).environment(\.multilineTextAlignment, .center).alignmentGuide(.bottom) { (ViewDimensions) -> CGFloat in
if self.height == nil {self.height = ViewDimensions.height}
return ViewDimensions.height
}.frame( height: (height ?? defaultHeight) * constHeightRatio, alignment: .bottom).background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent".extraLines
return
TextFieldDemo(content: .constant(content))
}
}
#endif
This works fine for single view. If view assembly is required (with other stacking views, etc), you may adjust defaultHeight and/or constHeightRatio to achieve what you want. Hopefully it works for you too.