I'm creating a watchOS complication using WidgetKit and SwiftUI. I'm trying to add use the date style for the Text view, but it uses a long version:
import SwiftUI
import WidgetKit
struct MyGaugeCircularAccessoryView: View {
let gradient = Gradient(stops: [.init(color: .red, location: 0), .init(color: .green, location: 0.25)])
var body: some View {
Gauge(value: 0.6) {
Text("ABC")
} currentValueLabel: {
Text(.now + 60 * 60 * 3, style: .relative)
}
.gaugeStyle(.accessoryCircular)
.tint(gradient)
}
}
struct MyGaugeCircularAccessoryView_Previews: PreviewProvider {
static var previews: some View {
MyGaugeCircularAccessoryView()
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
}
}
The is what it looks like:
Previously, I could create a complication like this:
CLKRelativeDateTextProvider(date: .now, style: .naturalAbbreviated, units: [.hour])
Is there a way to achieve this with the Text(date:style:) view?
Related
I am learning SwiftUI and right now I have problems understanding all those property wrappers.
I made this very simple progress view:
import Foundation
import SwiftUI
public struct VKProgressView : View
{
private var color: Color = Color.green
#State private var progress: CGFloat = 0.0
public init(value: Float)
{
self.progress = CGFloat(value)
}
public func color(_ color: Color) -> VKProgressView
{
var newView = self
newView.color = color
return newView
}
public var body: some View
{
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.frame(width: geometry.size.width, height: geometry.size.height)
.foregroundColor(Color.gray)
.opacity(0.30)
Rectangle()
.frame(width: geometry.size.width * self.progress, height: geometry.size.height)
.foregroundColor(self.color)
}
}
}
}
#if DEBUG
public struct VKProgressView_Previews: PreviewProvider
{
#State static var progress: Float = 0.75 // This is the value that changes in my real app.
public static var previews: some View
{
VKProgressView(value: self.progress)
.color(Color.accentColor)
}
}
#endif
However, when passing in some value, changing the value never updates the view. The property that is passed in has the #Published wrapper.
My workaround was to create a new ViewModel class that is instantiated in this progress view. The instance of the ViewModel has the ObservedObject and both properties have the #Published property wrapper. Although this works, I am thinking...this can't be right.
What am I missing here?
This is the working code (you can ignore the color property here):
import Foundation
import SwiftUI
public struct VKProgressView : View
{
#ObservedObject var viewModel: VKProgressViewViewModel
public init(value: Float, color: Color)
{
self.viewModel = VKProgressViewViewModel(progress: value, color: color)
}
public init(value: CGFloat, color: Color)
{
self.viewModel = VKProgressViewViewModel(progress: Float(value), color: color)
}
public init(value: Double, color: Color)
{
self.viewModel = VKProgressViewViewModel(progress: Float(value), color: color)
}
public var body: some View
{
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.frame(width: geometry.size.width, height: geometry.size.height)
.foregroundColor(Color.gray)
.opacity(0.30)
Rectangle()
.frame(width: geometry.size.width * self.viewModel.progress, height: geometry.size.height)
.foregroundColor(self.viewModel.color)
}
}
}
}
#if DEBUG
public struct VKProgressView_Previews: PreviewProvider
{
public static var previews: some View
{
VKProgressView(value: Float(0.5), color: Color.green)
}
}
#endif
And the ViewModel:
import Foundation
import SwiftUI
public class VKProgressViewViewModel : ObservableObject
{
#Published var progress: CGFloat = 0.0
#Published var color: Color = Color.accentColor
internal init(progress: Float, color: Color)
{
self.progress = CGFloat(progress)
self.color = color
}
}
In the second example, every time the "original" value changes, that was passed in, the view updates accordingly.
I am experiencing this issue with every single View I have created, so I think that I am simply missing something (fundamental).
Any help is appreciated.
#State is for internally managed properties of the view, that would trigger a redraw of that view. It is for value types, so when you pass in a value, the value is copied. SwiftUI maintains the value of #State independently of the specific instance of the view struct, because those structs are created and re-created frequently.
Your progress view is not likely to be updating the value of the progress amount, since it is simply reporting on the value it is given. It should just be let progress: CGFloat. Think of this as like a Text - you just give it a string to display, and it displays it.
Redrawing the view would be the responsibility of the next level up, which would own the progress state, and pass in the current value to your view:
#ObservedObject var model: SomeModelThatOwnsProgressAsAPublishedProperty
...
VKPRogressView(progress: model.progress)
or
#State var progress: CGFloat = 0
...
VKPRogressView(progress: progress)
In either case, changes to the progress would trigger a view redraw.
You haven't shown the code in your app where you are trying to pass in a value that isn't updated, so I can't comment on what exactly is going wrong, but a general rule of thumb is that a view with an #-something property will re-evaluate the body of itself (and therefore its subviews) when that property updates.
Views that don't own or update values should have them as let properties.
Views that own and update values should have them as #State properties.
Views that don't own but can update properties should have them as #Binding
"own" here refers to the source of truth for the property.
Using iOS 16.0 and Xcode 14.2
I am super new to SwiftUI and it honestly still hasn't really clicked yet, so this code probably is very inefficient. But basically I just want a button that when you press it, you go to another view. But when I try it like this, the preview crashes. Is there a better way to do this? And also what is causing the preview to crash? I've tried a bunch of different things and it either causes the preview to crash or the button just doesn't do anything.
Homescreen (had other buttons that were working that I redacted for clarity)
import SwiftUI
struct WordAndArrows: View {
#State private var showLibrary = false
var body: some View {
LibraryButton (action: {
self.navigateToLibraryScreen()
})
}
VStack {
if showLibrary {
LibraryView()
}
}
}
func navigateToLibraryScreen() {
self.showLibrary = true
}
}
struct WordAndArrows_Previews: PreviewProvider {
static var previews: some View {
ZStack {
GradientBackground()
WordAndArrows()
}
}
}
Library Button View
import SwiftUI
struct LibraryButton: View {
var action: () -> Void
var body: some View {
//Button("Check out my library") {
Button(action: action) {
Text("Library")
}
.padding()
.background(Color("Lime"))
.clipShape(Capsule())
.buttonStyle(SquishButtonStyle(fadeOnPress: false))
.font(Font.custom("Quicksand-Bold", size: 15))
.foregroundColor(Color("Indigo"))
.shadow(color: .gray, radius: 2.5, x: 0, y: 5)
}
}
I tried:
Making a navigatetolibraryscreen function that would be triggered when the buttn was hit
Using a navigationLink
3 Wrapping the navigation link in a Vstack
And it either caused the button to not do anything or the preview to crash
I am trying to implement drag and drop using the Transferable protocol within my app. I followed the below tutorial but can't get it to work. I created this very simple example and it still doesn't work. I must be missing something simple now.
https://serialcoder.dev/text-tutorials/swiftui/first-experience-with-transferable-implementing-drag-and-drop-in-swiftui/
import SwiftUI
import Foundation
import UniformTypeIdentifiers
struct ContentView: View {
#State private var draggedOutput: String = ""
var body: some View {
VStack(spacing: 50) {
Text("Text")
.font(.title)
.draggable("DRAGGED TEXT")
Text("Custom Transferable")
.font(.title)
.draggable(CustomTransferable())
Divider()
Text(draggedOutput)
.font(.largeTitle)
.frame(width: 250, height: 250)
.background(Color.green)
.dropDestination(for: String.self) { items, location in
draggedOutput = items.first!
return true
}
.dropDestination(for: CustomTransferable.self) { items, location in
draggedOutput = items.first!.id.uuidString
return true
}
}
.padding()
}
}
struct CustomTransferable: Identifiable, Codable, Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: CustomTransferable.self, contentType: UTType(exportedAs: "com.gmail.Me.D.ExtraInfoAudio"))
}
let id: UUID
init(id: UUID = UUID()) {
self.id = id
}
}
I switched the order of the dropDestination modifiers and my custom drop started to work and the standard String one stopped working. I don't see it in the documentation or the WWDC talk for Transferable but it seems that only 1 dropDestination works
Edited:
Sorry for the original long story, following is a short minimal reproducible standalone example I can think of:
import SwiftUI
extension View {
/// get view's size and do something with it.
func getSize(action: #escaping (CGSize) -> Void) -> some View {
overlay(GeometryReader{ geo in
emptyView(size: geo.size, action: action)
})
}
// private empty view
private func emptyView(
size : CGSize,
action: #escaping (CGSize) -> Void
) -> some View {
action(size) // ⭐️ side effect❗️
return Color.clear
}
}
struct MyView: View {
#State private var size = CGSize(width: 300, height: 200)
#State private var ratio: CGFloat = 1
var body: some View {
VStack {
Spacer()
cell
Spacer()
controls
}
}
var cell: some View {
Color.pink
.overlay {
VStack {
Text("(\(Int(size.width)), \(Int(size.height)))")
Text("aspect ratio: \(String(format: "%.02f", ratio))")
}
}
.getSize { size in
print(size)
// although it works fine in Xcode preview,
// it seems this line never runs in the built app.
// (aspect ratio is updated in preview, but not in the built app)
ratio = size.width / size.height
// not even a single line in the console when run in the app.
print(ratio)
}
.frame(width: size.width, height: size.height)
}
var controls: some View {
VStack {
Slider(value: $size.width, in: 50...300, step: 1)
Slider(value: $size.height, in: 50...300, step: 1)
}
.padding(40)
}
}
Now the code above behaves differently in the Xcoe preview and the built app:
My question is why the built app is not updating the "ratio" part in the UI?
original long story below:
I was doing some custom layout for an array of items, and used GeometryReader to read the proposed size from parent and then tried to update some view states based on that size.
It worked perfectly fine in the Xcode preview, but failed to update (some) view states in the built app, as you can see in the following GIF:
The following code is used in the preview:
struct ItemsView_Previews: PreviewProvider {
static var previews: some View {
ItemsView()
.preferredColorScheme(.dark)
}
}
and the following is for the app's content view:
struct ContentView: View {
var body: some View {
ItemsView()
.overlay {
Text("Built App")
.font(.largeTitle)
.bold()
.foregroundColor(.orange)
.opacity(0.3)
.shadow(radius: 2)
}
}
}
as you can see, they both use exactly the same ItemsView, which is defined by the following code:
import SwiftUI
struct ItemsView: View {
#State private var size = CGSize(300, 300) // proposed size
#State private var rows = 0 // current # of rows
#State private var cols = 0 // current # of cols
#State private var ratio: CGFloat = 1 // current cell aspect ratio
#State private var idealRatio: CGFloat = 1 // ideal cell aspect ratio
let items = Array(1...20)
var body: some View {
VStack {
ScrollView {
itemsView // view for layed out items
}
controls // control size, cell ratio
}
.padding()
}
}
extension ItemsView {
/// a view with layed out item views
var itemsView: some View {
// layout item views
items.itemsView { size in // Array+ .itemsView()
// ⭐ inject layout instance
RatioRetainingLayout( // RatioRetainingLayout
idealRatio,
count: items.count,
in: size
)
} itemView: { i in
// ⭐ inject view builder
Color(hue: 0.8, saturation: 0.8, brightness: 0.5)
.padding(1)
.overlay {
Text("\(i)").shadow(radius: 2)
}
}
// ⭐ get proposed size from parent
.getSize { proposedSize in // 🌀View+ .getSize()
// ⭐ recompute layout
let layout = RatioRetainingLayout( // 👔 RatioRetainingLayout
idealRatio,
count: items.count,
in: proposedSize
)
// ⭐ update view states
rows = layout.rows
cols = layout.cols
ratio = layout.cellSize.aspectRatio // 🅿️ Vector2D: CGSize+ .aspectRatio
}
// ⭐ proposed size
.frame(size) // 🌀View+ .frame(size), .dimension()
.dimension(.topLeading, arrow: .blue, label: .orange)
.padding(4)
.shadowedBorder() // 🌀View+ .shadowedBorder()
.padding(40)
}
/// sliders to control proposed size, ideal ratio
var controls: some View {
SizeRatioControl( // 👔 SizeRatioControl
size: $size,
rows: $rows,
cols: $cols,
idealRatio: $idealRatio,
ratio: $ratio
)
}
}
I used some custom extensions, protocols and types to support the ItemsView struct, but I think they are not relevant, if you're interested, you can have a look at GitHub.
I think the most relevant part in the above code is the following, where it tries to update some view states with respect to the proposed size:
// ⭐ get proposed size from parent
.getSize { proposedSize in // 🌀View+ .getSize()
// ⭐ recompute layout
let layout = RatioRetainingLayout( // 👔 RatioRetainingLayout
idealRatio,
count: items.count,
in: proposedSize
)
// ⭐ update view states
rows = layout.rows
cols = layout.cols
ratio = layout.cellSize.aspectRatio // 🅿️ Vector2D: CGSize+ .aspectRatio
}
and the .getSize() part is a custom View extension which I used to get the proposed size from parent by using GeometryReader:
extension View {
/// get view's size and do something with it.
func getSize(action: #escaping (CGSize) -> Void) -> some View {
background(GeometryReader{ geo in
emptyView(size: geo.size, action: action)
})
}
// private empty view
private func emptyView(
size : CGSize,
action: #escaping (CGSize) -> Void
) -> some View {
action(size) // ⭐️ side effect❗️
return EmptyView()
}
}
While everything works fine in the Xcode preview, sadly it just doesn't work in the built app.
Am I doing something wrong with the SwiftUI view states? Please help. Thanks.
I finally come to realize that I've been keeping violating the most important rule in SwiftUI - the "source of truth" rule.
I shouldn't have made the ratio a #State private var in the first place, its value totally depends on size, and that means ratio should be a computed property instead.
So, with the following revision, everything works just fine:
(we don't even need the orginal .getSize() extension)
struct MyView: View {
// ⭐️ source of truth
#State private var size = CGSize(width: 300, height: 200)
// ⭐️ computed property
var ratio: CGFloat { size.width / size.height }
var body: some View {
VStack {
Spacer()
cell
Spacer()
controls
}
}
var cell: some View {
Color.pink
.overlay {
VStack {
Text("(\(Int(size.width)), \(Int(size.height)))")
Text("aspect ratio: \(String(format: "%.02f", ratio))")
}
}
.frame(width: size.width, height: size.height)
}
var controls: some View {
VStack {
Slider(value: $size.width, in: 50...300, step: 1)
Slider(value: $size.height, in: 50...300, step: 1)
}
.padding(40)
}
}
Sometimes I any need to adjust the swiftUI component, so I dive into UIKit and adjust what I need, for example in the code below I adjust the table view appearance for a specific view, the problem is that effect to all the SwiftUI views the have table view so everything became a big mess, did there is some way to adjust the SwiftUI component for specific view without effect the same component in the other's views.
struct New_EditGroup: View {
init() {
let tableAppearance = UITableView.appearance()
tableAppearance.contentInset = UIEdgeInsets(top: 16, left: 0, bottom: 0, right: 0)
tableAppearance.sectionHeaderHeight = 1
tableAppearance.sectionFooterHeight = 16
}
var body: some View {
// ...
struct ContentView: View {
var body: some View {
NavigationView {
Form {
Section(header: Spacer().frame(height: 16)) {
ForEach(1..<10) { element in
Text("Element \(element)")
}
}
}
.navigationBarTitle("Main Menu", displayMode: .inline)
}
}
}