I'm bashing my face against a wall trying to figure out why I can't get the .digitalCrownRotation feature to work on a Text UI Component in SwiftUI for WatchOS.
Here's my code:
import SwiftUI
struct ButtonView: View {
#State private var isFocused = false
#State private var value: Int = 0
var body: some View {
Button(action: {
print("clicked")
}) {
Text("\(value)")
.contentShape(Rectangle())
.focusable { self.isFocused = $0 }
.digitalCrownRotation(self.$value, from: 0, through: 9, by: 1, sensitivity: .medium, isContinuous: true, isHapticFeedbackEnabled: true)
}
.background(self.isFocused ? Color.green : Color.white)
}
}
Everything worked fine up until the pointer where I tried to add the .digitalCrownRotation functionality.
Whenever I try to build I'm faced with the following 2 build fail messages:
Argument type 'Int.Stride' (aka 'Int') does not conform to expected type 'BinaryFloatingPoint'
Argument type 'Int' does not conform to expected type 'BinaryFloatingPoint'
I'm basically trying to use the digital crown to step through numbers (integers) from 0 to 9 when the buttons are focused. But it's not working and I'm not sure what to do to resolve it.
Here is declaration
public func digitalCrownRotation<V>(_ binding: Binding<V>, from minValue: V, through
maxValue: V, by stride: V.Stride? = nil, sensitivity: DigitalCrownRotationalSensitivity = .high,
isContinuous: Bool = false, isHapticFeedbackEnabled: Bool = true) -> some View
where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint ```
so as seen and error reports V type (which is detected from your provided binding) should be floating point, but your value is Int, which is not floating point, so fix is simple
#State private var value: Double = 0
Related
I wrote a demo program to display pins on a map in SwiftUI following this and this tutorial and it worked no problem. Then when I applied this same technique to my app, I get an error that the type cannot conform to 'MapAnnotationProtocol'.
Here's my code:
import SwiftUI
import MapKit
struct MapLocationView: View {
#ObservedObject var manager = LocationManager()
#FetchRequest(sortDescriptors: [])
private var meals: FetchedResults<Meal>
var body: some View {
Map(coordinateRegion: $manager.region, annotationItems: meals) { meal in // error is on Map
if (meal.latitude != 0 && meal.longitude != 0) {
let coordinate = CLLocationCoordinate2D(latitude: meal.latitude, longitude: meal.longitude)
MapPin(coordinate: coordinate)
}
}
}
}
The LocationManager code is the same, unaltered code from the tutorial and from the demo I wrote. I don't understand why it works in the demo and doesn't work in my app.
I've tried wrapping the code in a Group {} but that didn't fix it. I've read other articles with similar error messages but none of those solutions seemed to apply in this case.
I'm pretty new to Swift and SwiftUI so it makes no sense to me that it should work in the demo but not in my app. I hope I've provided enough information for the problem to make sense.
A couple of things come to mind while looking at your code:
Does Meal conform to Identifiable?
Your closure should always return some sort of map annotation.
Your MapPin(.. ) should work, but it looks like you'll need to filter your array of Meal structs before that closure is called.
The following is untested code, but might help anyway.
extension Meal: Identifiable {
var id: ObjectIdentifier {
return ObjectIdentifier(self)
}
}
extension Meal {
var isValid: Bool {
return latitude != 0 && longitude != 0
}
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
var body: some View {
Map(coordinateRegion:
$manager.region,
annotationItems: meals.filter { $0.isValid ) { meal in
MapPin(coordinate: meal.coordinate)
}
}
Why running this code shows "Fatal error: Index out of range"?
import SwiftUI
struct MyData {
var numbers = [Int](repeating: 0, count: 5)
}
#main
struct TrySwiftApp: App {
#State var myData = MyData()
var body: some Scene {
WindowGroup {
ChildView(myData: myData)
.frame(width: 100, height: 100)
.onAppear {
myData.numbers.removeFirst() // change myData
}
}
}
}
struct ChildView: View {
let myData: MyData // a constant
var body: some View {
ForEach(myData.numbers.indices) {
Text("\(myData.numbers[$0])") // Thread 1: Fatal error: Index out of range
}
}
}
After checking other questions,
I know I can fix it by following ways
// fix 1: add id
ForEach(myData.numbers.indices, id: \.self) {
//...
}
or
// Edited:
//
// This is not a fix, see George's reply
//
// fix 2: make ChildView conforms to Equatable
struct ChildView: View, Equatable {
static func == (lhs: ChildView, rhs: ChildView) -> Bool {
rhs.myData.numbers == rhs.myData.numbers
}
...
My Questions:
How a constant value (defined by let) got out of sync?
What ForEach really did?
Let me give you a simple example to show you what happened:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
if you look at the upper code you would see that I am initializing a ForEach JUST with a Range like this: lowerBound..<11 which it means this 0..<11, when you do this you are telling SwiftUI, hey this is my range and it will not change! It is a constant Range! and SwiftUI says ok! if you are not going update upper or lower bound you can use ForEach without showing or given id! But if you see my code again! I am updating lowerBound of ForEach and with this action I am breaking my agreement about constant Range! So SwiftUI comes and tell us if you are going update my ForEach range in count or any thing then you have to use an id then you can update the given range! And the reason is because if we have 2 same item with same value, SwiftUI would have issue to know which one you say! with using an id we are solving the identification issue for SwiftUI! About id you can use it like this: id:\.self or like this id:\.customID if your struct conform to Hash-able protocol, or in last case you can stop using id if you confrom your struct to identifiable protocol! then ForEach would magically sink itself with that.
Now see the edited code, it will build and run because we solved the issue of identification:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11, id:\.self) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
Things go wrong when you do myData.numbers.removeFirst(), because now myData.numbers.indices has changed and so the range in the ForEach showing Text causes problems.
You should see the following warning (at least I do in Xcode 13b5) hinting this could cause issues:
Non-constant range: not an integer range
The reason it is not constant is because MyData's numbers property is a var, not let, meaning it can change / not constant - and you do change this. However the warning only shows because you aren't directly using a range literal in the ForEach initializer, so it assumes it's not constant because it doesn't know.
As you say, you have some fixes. Solution 1 where you provide id: \.self works because now it uses a different initializer. Definition for the initializer you are using:
#available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ForEach where Data == Range<Int>, ID == Int, Content : View {
/// Creates an instance that computes views on demand over a given constant
/// range.
///
/// The instance only reads the initial value of the provided `data` and
/// doesn't need to identify views across updates. To compute views on
/// demand over a dynamic range, use ``ForEach/init(_:id:content:)``.
///
/// - Parameters:
/// - data: A constant range.
/// - content: The view builder that creates views dynamically.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
}
Stating:
The instance only reads the initial value of the provided data and doesn't need to identify views across updates. To compute views on demand over a dynamic range, use ForEach/init(_:id:content:).
So that's why your solution 1 worked. You switched to the initializer which didn't assume the data was constant and would never change.
Your solution 2 isn't really a "solution". It just doesn't update the view at all, because myData.numbers changes so early that it is always equal, so the view never updates. You can see the view still has 5 lines of Text, rather than 4.
If you still have issues with accessing the elements in this ForEach and get out-of-bounds errors, this answer may help.
I am working on a money input screen and I need to implement a custom init to set a state variable based on the initialized amount.
I thought the following would work:
struct AmountView : View {
#Binding var amount: Double
#State var includeDecimal = false
init(amount: Binding<Double>) {
self.amount = amount
self.includeDecimal = round(amount)-amount > 0
}
}
However, this gives me a compiler error as follows:
Cannot assign value of type 'Binding' to type 'Double'
How do I implement a custom init method which takes in a Binding struct?
Argh! You were so close. This is how you do it. You missed a dollar sign (beta 3) or underscore (beta 4), and either self in front of your amount property, or .value after the amount parameter. All these options work:
You'll see that I removed the #State in includeDecimal, check the explanation at the end.
This is using the property (put self in front of it):
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4
self.includeDecimal = round(self.amount)-self.amount > 0
}
}
or using .value after (but without self, because you are using the passed parameter, not the struct's property):
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4
self.includeDecimal = round(amount.value)-amount.value > 0
}
}
This is the same, but we use different names for the parameter (withAmount) and the property (amount), so you clearly see when you are using each.
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4
self.includeDecimal = round(self.amount)-self.amount > 0
}
}
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4
self.includeDecimal = round(withAmount.value)-withAmount.value > 0
}
}
Note that .value is not necessary with the property, thanks to the property wrapper (#Binding), which creates the accessors that makes the .value unnecessary. However, with the parameter, there is not such thing and you have to do it explicitly. If you would like to learn more about property wrappers, check the WWDC session 415 - Modern Swift API Design and jump to 23:12.
As you discovered, modifying the #State variable from the initilizer will throw the following error: Thread 1: Fatal error: Accessing State outside View.body. To avoid it, you should either remove the #State. Which makes sense because includeDecimal is not a source of truth. Its value is derived from amount. By removing #State, however, includeDecimal will not update if amount changes. To achieve that, the best option, is to define your includeDecimal as a computed property, so that its value is derived from the source of truth (amount). This way, whenever the amount changes, your includeDecimal does too. If your view depends on includeDecimal, it should update when it changes:
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal: Bool {
return round(amount)-amount > 0
}
init(withAmount: Binding<Double>) {
self.$amount = withAmount
}
var body: some View { ... }
}
As indicated by rob mayoff, you can also use $$varName (beta 3), or _varName (beta4) to initialise a State variable:
// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
You should use underscore to access the synthesized storage for the property wrapper itself.
In your case:
init(amount: Binding<Double>) {
_amount = amount
includeDecimal = round(amount)-amount > 0
}
Here is the quote from Apple document:
The compiler synthesizes storage for the instance of the wrapper type by prefixing the name of the wrapped property with an underscore (_)—for example, the wrapper for someProperty is stored as _someProperty. The synthesized storage for the wrapper has an access control level of private.
Link: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> propertyWrapper section
You said (in a comment) “I need to be able to change includeDecimal”. What does it mean to change includeDecimal? You apparently want to initialize it based on whether amount (at initialization time) is an integer. Okay. So what happens if includeDecimal is false and then later you change it to true? Are you going to somehow force amount to then be non-integer?
Anyway, you can't modify includeDecimal in init. But you can initialize it in init, like this:
struct ContentView : View {
#Binding var amount: Double
init(amount: Binding<Double>) {
$amount = amount
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
}
#State private var includeDecimal: Bool
(Note that at some point the $$includeDecimal syntax will be changed to _includeDecimal.)
Since it's mid of 2020, let's recap:
As to #Binding amount
_amount is only recommended to be used during initialization. And never assign like this way self.$amount = xxx during initialization
amount.wrappedValue and amount.projectedValue are not frequently used, but you can see cases like
#Environment(\.presentationMode) var presentationMode
self.presentationMode.wrappedValue.dismiss()
A common use case of #binding is:
#Binding var showFavorited: Bool
Toggle(isOn: $showFavorited) {
Text("Change filter")
}
State:
To manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body and You should only access a state property from inside the view’s body, or from methods called.
Note: To pass a state property to another view in the view hierarchy, use the variable name with the $ prefix operator.
struct ContentView: View {
#State private var isSmile : Bool = false
var body: some View {
VStack{
Text(isSmile ? "😄" : "😭").font(.custom("Arial", size: 120))
Toggle(isOn: $isSmile, label: {
Text("State")
}).fixedSize()
}
}
}
Binding:
The parent view declares a property to hold the isSmile state, using the State property wrapper to indicate that this property is the value’s source of deferent view.
struct ContentView: View {
#State private var isSmile : Bool = false
var body: some View {
VStack{
Text(isSmile ? "😄" : "😭").font(.custom("Arial", size: 120))
SwitchView(isSmile: $isSmile)
}
}
}
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.
struct SwitchView: View {
#Binding var isSmile : Bool
var body: some View {
VStack{
Toggle(isOn: $isSmile, label: {
Text("Binding")
}).fixedSize()
}
}
}
The accepted answer is one way but there is another way too
struct AmountView : View {
var amount: Binding<Double>
init(withAmount: Binding<Double>) {
self.amount = withAmount
}
var body: some View { ... }
}
You remove the #Binding and make it a var of type Binding
The tricky part is while updating this var. You need to update it's property called wrapped value. eg
amount.wrappedValue = 1.5 // or
amount.wrappedValue.toggle()
You can achieve this either with static function or with custom init.
import SwiftUI
import PlaygroundSupport
struct AmountView: View {
#Binding var amount: Double
#State var includeDecimal: Bool
var body: some View {
Text("The amount is \(amount). \n Decimals \(includeDecimal ? "included" : "excluded")")
}
}
extension AmountView {
static func create(amount: Binding<Double>) -> Self {
AmountView(amount: amount, includeDecimal: round(amount.wrappedValue) - amount.wrappedValue > 0)
}
init(amount: Binding<Double>) {
_amount = amount
includeDecimal = round(amount.wrappedValue) - amount.wrappedValue > 0
}
}
struct ContentView: View {
#State var amount1 = 5.2
#State var amount2 = 5.6
var body: some View {
AmountView.create(amount: $amount1)
AmountView(amount: $amount2)
}
}
PlaygroundPage.current.setLiveView(ContentView())
Actually you don't need custom init here at all since the logic could be easily moved to .onAppear unless you need to explicitly set initial state externally.
struct AmountView: View {
#Binding var amount: Double
#State private var includeDecimal = true
var body: some View {
Text("The amount is \(amount, specifier: includeDecimal ? "%.3f" : "%.0f")")
Toggle("Include decimal", isOn: $includeDecimal)
.onAppear {
includeDecimal = round(amount) - amount > 0
}
}
}
This way you keep your #State private and initialized internally as documentation suggests.
Don’t initialize a state property of a view at the point in the view
hierarchy where you instantiate the view, because this can conflict
with the storage management that SwiftUI provides. To avoid this,
always declare state as private, and place it in the highest view in
the view hierarchy that needs access to the value
.
I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String
I've stumbled across this piece of code:
class TextLimiter: ObservableObject {
private let limit: Int
init(limit: Int) {
self.limit = limit
}
#Published var value = "" {
didSet {
if value.count > self.limit {
value = String(value.prefix(self.limit))
self.hasReachedLimit = true
} else {
self.hasReachedLimit = false
}
}
}
#Published var hasReachedLimit = false }
struct Strix: View {
#ObservedObject var input = TextLimiter(limit: 5)
var body: some View {
TextField("Text Input",
text: $input.value)
.border(Color.red,
width: $input.hasReachedLimit.wrappedValue ? 1 : 0 )
} }
It's a TextField limiting code where after a user inputs characters after a limit, it won't keep inputing characters inside the box. I've tried this code and after the limit is reached, it just keeps on inputting characters.
For example:
How it's supposed to work: limit is 5 so the only input allowed is 'aaaaa'
How it's behaving: limit is 5 but input allowed is 'aaaaaaaa.....'
I'm aware of a recent solution to this:
How to set textfield character limit SwiftUI?
but the solution is specifically tailored for iOS 14. I was hoping to be able to support iOS 13. Thanks.
Link to original code:
https://github.com/programmingwithswift/SwiftUITextFieldLimit/blob/master/SwiftUITextFieldLimit/SwiftUITextFieldLimit/ContentView.swift
Your solution is lies in SwiftUI's subscriber .onReceive,
Make sure that your property hasReachedLimit must not marked with #Published else it will trigger infinite loop of view body rendering.
Below shown code works as your expectation.
class TextLimiter: ObservableObject {
let limit: Int
#Published var value = ""
var hasReachedLimit = false
init(limit: Int) {
self.limit = limit
}
}
struct Strix: View {
#ObservedObject var input = TextLimiter(limit: 5)
var body: some View {
TextField("Text Input",
text: $input.value)
.border(Color.red,
width: $input.hasReachedLimit.wrappedValue ? 1 : 0 )
.onReceive(Just(self.input.value)) { inputValue in
self.input.hasReachedLimit = inputValue.count > self.input.limit
if inputValue.count > self.input.limit {
self.input.value.removeLast()
}
}
}
}
BTW this is not an efficient solution.