I'm still trying to get my head around Swift/SwiftUI, so apologies for what's likely a really simple question. Consider this simple SwiftUI file:
import SwiftUI
struct sampleStruct {
let entryID: Int // comes from the system
let name: String
}
struct TestView: View {
var listEntries = [sampleStruct(entryID: 301, name: "Pete"),
sampleStruct(entryID: 5003, name: "Taylor"),
sampleStruct(entryID: 13, name: "Suzie")]
var body: some View {
VStack{
ForEach(listEntries, id: \.entryID) { idx in
Text(idx.name)
}
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Here's my question: I'm looking for a simple way to add the position of the element in the array to the text field. I.e. instead of Pete, Taylor, Susie, I'd like to get:
1 Pete
2 Taylor
3 Suzie
I thought that this could do the trick:
...
VStack{
ForEach(listEntries, id: \.entryID) { idx in
Text( listEntries.firstIndex { $0.id == idx.entryID } )
Text(idx.name)
}
}
...
But, I'm getting the error "No exact matches in call to initializer".
Any help would be appreciated,
Philipp
The simplest approach would be to use enumerated(), which returns a sequence of pairs (index and item):
struct TestView: View {
var listEntries = [sampleStruct(entryID: 301, name: "Pete"),
sampleStruct(entryID: 5003, name: "Taylor"),
sampleStruct(entryID: 13, name: "Suzie")]
var body: some View {
VStack{
ForEach(Array(listEntries.enumerated()), id: \.1.entryID) { (index,item) in
Text("\(index + 1) \(item.name)")
}
}
}
}
Note that for extremely large collections you might want a different approach for performance reasons, but for anything reasonably sized, this will work well. A quick and easy upgrade to it would be to used indexed() from Swift Algorithms in place of .enumerated() (you could omit the Array(...) wrapper if you did this as well.
If for learning purposes you want to see what you'd have to do to adjust your original approach, you'd want to:
Use .entryID instead of .id, which doesn't exist on your model
Provide a default index in case firstIndex returns nil
Interpolate the result into a String
One solution might look like this:
struct TestView: View {
var listEntries = [sampleStruct(entryID: 301, name: "Pete"),
sampleStruct(entryID: 5003, name: "Taylor"),
sampleStruct(entryID: 13, name: "Suzie")]
var body: some View {
VStack{
ForEach(listEntries, id: \.entryID) { item in
let index = listEntries.firstIndex { $0.entryID == item.entryID } ?? -1
Text("\(index + 1) \(item.name)")
}
}
}
}
Related
I'm curious, how do we specify a binding to State data that is part of an optional? For instance:
struct NameRecord {
var name = ""
var isFunny = false
}
class AppData: ObservableObject {
#Published var nameRecord: NameRecord?
}
struct NameView: View {
#StateObject var appData = AppData()
var body: some View {
Form {
if appData.nameRecord != nil {
// At this point, I *know* that nameRecord is not nil, so
// I should be able to bind to it.
TextField("Name", text: $appData.nameRecord.name)
Toggle("Is Funny", isOn: $appData.nameRecord.isFunny)
} else {
// So far as I can tell, this should never happen, but
// if it does, I will catch it in development, when
// I see the error message in the constant binding.
TextField("Name", text: .constant("ERROR: Data is incomplete!"))
Toggle("Is Funny", isOn: .constant(false))
}
}
.onAppear {
appData.nameRecord = NameRecord(name: "John")
}
}
}
I can certainly see that I'm missing something. Xcode gives errors like Value of optional type 'NameRecord?' must be unwrapped to refer to member 'name' of wrapped base type 'NameRecord') and offers some FixIts that don't help.
Based on the answer from the user "workingdog support Ukraine" I now know how to make a binding to the part I need, but the solution doesn't scale well for a record that has many fields of different type.
Given that the optional part is in the middle of appData.nameRecord.name, it seems that there might be a solution that does something like what the following function in the SwiftUI header might be doing:
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get }
My SwiftFu is insufficient, so I don't know how this works, but I suspect it's what is doing the work for something like $appData.nameRecord.name when nameRecord is not an optional. I would like to have something where this function would result in a binding to .constant when anything in the keyPath is nil (or even if it did a fatalError that I would avoid with conditionals as above). It would be great if there was a way to get a solution that was as elegant as Jonathan's answer that was also suggested by workingdog for a similar situation. Any pointers in that area would be much appreciated!
Binding has a failable initializer that transforms a Binding<Value?>.
if let nameRecord = Binding($appData.nameRecord) {
TextField("Name", text: nameRecord.name)
Toggle("Is Funny", isOn: nameRecord.isFunny)
} else {
Text("Data is incomplete")
TextField("Name", text: .constant(""))
Toggle("Is Funny", isOn: .constant(false))
}
Or, with less repetition:
if appData.nameRecord == nil {
Text("Data is incomplete")
}
let bindings = Binding($appData.nameRecord).map { nameRecord in
( name: nameRecord.name,
isFunny: nameRecord.isFunny
)
} ?? (
name: .constant(""),
isFunny: .constant(false)
)
TextField("Name", text: bindings.name)
Toggle("Is Funny", isOn: bindings.isFunny)
I want to link to a view that contains a non-view object - created once per user tap of the "Start" link - that is dependent on data selected by the user. The code below is as close as I've gotten. QuestionView.init is called as soon as HomeView appears, and again every time you select a new value for Highest Number, thus creating the Question object repeatedly, which is what I'm trying to avoid. I want to only create the Question object one time - when the user taps on the "Start" link.
I've tried many different approaches. It feels like I am stuck problem solving from an imperative UI oriented approach, instead of the new-for-me declarative approach of SwiftUI. Perhaps there's a bridge I'm missing from the state-driven approach of Views to the more familiar-to-me non-view objects, like my Question object I want to create only once.
struct Question {
let value1: Int
let value2: Int
let answer: Int
init(_ highestNumber: Int) {
print("Question.init \(highestNumber)")
value1 = Int.random(in: 1...highestNumber)
value2 = Int.random(in: 1...highestNumber)
answer = value1 * value2
}
var prompt: String {
"\(value1) x \(value2) = ?"
}
}
struct HomeView: View {
#State var highestNumber: Int = 12
var body: some View {
NavigationStack {
Picker("Highest Number", selection: $highestNumber) {
ForEach(4...12, id: \.self) { Text(String($0)) }
}
.pickerStyle(.wheel)
NavigationLink(destination: QuestionView(highestNumber: $highestNumber)) {
Text("Start")
}
}
}
}
struct QuestionView: View {
#Binding var highestNumber: Int
#State var question: Question
init(highestNumber: Binding<Int>) {
print("QuestionView.init")
self._highestNumber = highestNumber
question = Question(highestNumber.wrappedValue)
}
var body: some View {
Text(question.prompt)
Button("I got it") {
question = Question(highestNumber)
}
}
}
I' trying to create a set of Toggles, that need to be stored in one core data field of type "Transformable". I started with this example:
https://developer.apple.com/forums/thread/118595
in combination with other ideas from stack.
I'm trying to get this way:
Create a Set of structs like this
struct AllScopes: Identifiable, Hashable {
var id: UUID
var name: String
var notify: Bool
}
[...]
// all the stuff with View and body with
#State var scopes = Set<AllScopes>()
[...]
// and here I run through my FetchRequest to fill the Set
.onAppear {
for scope in allScopes {
scopes.insert(
AllScopes(
id: scope.id!,
name: scope.name!,
notify: false
)
)
}
}
In the end I've got a nice Set with all my scopes.
I call a new View with YearlyReportPage6(scopes: $scopes)
And now my problem - the next view:
struct YearlyReportPage6: View {
#Binding var scopes: Set<AllScopes>
init(scopes: Binding<Set<AllScopes>>) {
_scopes = scopes
}
var body: some View {
VStack {
ForEach(scopes.indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
}
}
}
But all in ForEach creates errors. Either Binding in isOn: is wrong, or ForEach can't work with the set, or the Text is not a String, or, or, or...
In the end there should be a list of Toggles (checkboxes) and the selection should be stored in database.
Changing the Set into a simple Array like #State var scopes = [AllScopes]() will work for the Toggles, but how can I store this into a Transformable?
ForEach(Array(scopes).indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
To summarize:
either how can I create the list of Toggles with the Set of AllScopes
or how can I store the Array / Dictionary into the Transformable field?
I hope, you can understand my clumsy English. :-)
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
Can SwiftUI Text Fields work with optional Bindings? Currently this code:
struct SOTestView : View {
#State var test: String? = "Test"
var body: some View {
TextField($test)
}
}
produces the following error:
Cannot convert value of type 'Binding< String?>' to expected argument type 'Binding< String>'
Is there any way around this? Using Optionals in data models is a very common pattern - in fact it's the default in Core Data so it seems strange that SwiftUI wouldn't support them
You can add this operator overload, then it works as naturally as if it wasn't a Binding.
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}
This creates a Binding that returns the left side of the operator's value if it's not nil, otherwise it returns the default value from the right side.
When setting it only sets lhs value, and ignores anything to do with the right hand side.
It can be used like this:
TextField("", text: $test ?? "default value")
Ultimately the API doesn't allow this - but there is a very simple and versatile workaround:
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
public var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue.isEmpty ? nil : newValue
}
}
}
This allows you to keep the optional while making it compatible with Bindings:
TextField($test.bound)
True, at the moment TextField in SwiftUI can only be bound to String variables, not String?.
But you can always define your own Binding like so:
import SwiftUI
struct SOTest: View {
#State var text: String?
var textBinding: Binding<String> {
Binding<String>(
get: {
return self.text ?? ""
},
set: { newString in
self.text = newString
})
}
var body: some View {
TextField("Enter a string", text: textBinding)
}
}
Basically, you bind the TextField text value to this new Binding<String> binding, and the binding redirects it to your String? #State variable.
I prefer the answer provided by #Jonathon. as it is simple and elegant and provides the coder with an insitu base case when the Optional is .none (= nil) and not .some.
However I feel it is worth adding in my two cents here. I learned this technique from reading Jim Dovey's blog on SwiftUI Bindings with Core Data. Its essentially the same answer provided by #Jonathon. but does include a nice pattern that can be replicated for a number of different data types.
First create an extension on Binding
public extension Binding where Value: Equatable {
init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) {
self.init(
get: { source.wrappedValue ?? nilProxy },
set: { newValue in
if newValue == nilProxy { source.wrappedValue = nil }
else { source.wrappedValue = newValue }
}
)
}
}
Then use in your code like this...
TextField("", text: Binding($test, replacingNilWith: String()))
or
TextField("", text: Binding($test, replacingNilWith: ""))
Try this works for me with reusable function
#State private var name: String? = nil
private func optionalBinding<T>(val: Binding<T?>, defaultVal: T)-> Binding<T>{
Binding<T>(
get: {
return val.wrappedValue ?? defaultVal
},
set: { newVal in
val.wrappedValue = newVal
}
)
}
// Usage
TextField("", text: optionalBinding(val: $name, defaultVal: ""))