I am trying to present my Enum cases as a List choice in SwiftUI. I am using Xcode 13.2.1
My problem is binding the state variable of the enum type to the List choice. I get the compiler error
"Generic parameter 'SelectionValue' could not be inferred"
enum MyEnum : String, CaseIterable, Identifiable
{
var id : RawValue { rawValue }
case A = "_A_"
case B = "_B_"
case C = "_C_"
}
struct MyView : View
{
#State var choice : MyEnum = MyEnum.A
var body : some View
{
List( MyEnum.allCases , selection: $choice) // << Generic parameter error here
{ name in
Text(name.rawValue)
}
}
}
Attempting to specify the generic parameters does not seem to help me either.
For example I tried
List<MyEnum, ForEach<[MyEnum], String, Text>>( MyEnum.allCases , selection: $choice)
But get the same compiler error
The List selection type required to be optional. Here is fixed part
struct MyView : View
{
#State var choice : MyEnum? = MyEnum.A // << here !!
// .. other content
}
Related
Let's say I have 2 entities:
GameSession :which has Attributes "date", "place", "numberofplayer" + a relationship called "players" with "Player"
Player: which has Attributes "name","score_part1","score_part2","score_part3" + a relationship with "GameSession"
the relationship is "one to many": One session can have many players
Let's say now I have a list of GameSession and when I click on on one (with a NavigationLink)
It sends me to a new view where I can see:
All the names of the players of that session (in text) and also right next to the player name I would like to have 3 TextField in which I can enter (an update) "score_part1","score_part2","score_part3" for every players of that session
Basically I am able to display the name of all the players of a given session, But it seems impossible to have the "score_part1","score_part2","score_part3" in editable TextField...
I have an error saying "Cannot convert value of type 'String' to expected argument type 'Binding<String>'"
Basically in my first view I have something like that:
struct RamiListePartieUIView: View {#Environment(.managedObjectContext) var moc#FetchRequest(entity: GameSession.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \GameSession.date, ascending: false)]) var gamesessions: FetchedResults<GameSession>
var body: some View {
VStack {
List {
ForEach(gamesessions, id: \.date) { session in
NavigationLink (destination: DetailPartieSelecUIView(session: session)){
Text("\(session.wrappedPlace) - le \(session.wrappedDate, formatter: itemFormatter) ")
}
}
.onDelete(perform: deleteSessions)
.padding()
}
}
}
}
And in my second view I have something like that:
struct DetailPartieSelecUIView: View {
#State var session:GameSession
#Environment(\.managedObjectContext) var moc
var body: some View {
Section("Ma session du \(session.wrappedDate, formatter: itemFormatter)"){
ForEach(session.playersArray, id: \.self) { player in
HStack {
Text(player.wrappedName) // OK it works
TextField("score", text : player.wrappedScore_part1) // it generates an error
TextField("score", text : player.wrappedScore_part2) // it generates an error
TextField("score", text : player.wrappedScore_part3) // it generates an error
}
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .short
// formatter.timeStyle = .medium
formatter.dateFormat = "YYYY/MM/dd" //"YY/MM/dd"
return formatter
}()
also,
I have defined the "wrappedScore_part1","wrappedScore_part2","wrappedScore_part3" in the Player+CoreDataProperties.swift file
and "wrappedPlace", "wrappedData" as well as the "PlayersArray" in the GameSession+CoreDataProperties.swift file
it is done like that:
public var wrappedPlace: String {
place ?? "Unknown"
}
// Convert NSSet into an array of "Player" object
public var playersArray: [Player] {
let playersSet = players as? Set<Player> ?? []
return playersSet.sorted {
$0.wrappedName< $1.wrappedName
}
}
I am new at coding with swiftUI so I am probably doing something wrong... If anyone can help me it would be much appreciated.
Thanks a lot
I have tried a lot of things. Like changing the type of my attribute to Int32 instead os String. As I am suppose to enter numbers in those fields, I thought it would be best to have Integer. But it didn't change anything. and ultimately I had the same kind of error message
I tried also to add the $ symbol, like that:
TextField("score", text : player.$wrappedScore_part1)
But then I had other error message popping up at the row of my "ForEach", saying "Cannot convert value of type '[Player]' to expected argument type 'Binding'"
And also on the line just after the HStack, I had an error saying "Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'"
Thank you for your help!
Best regards,
JB
Your first problem of how to fetch the players in a session you need to supply a predicate to the #FetchRequest<Player>, e.g.
#FetchRequest
private var players: FetchedResults<Player>
init(session: Session) {
let predicate = NSPredicate(format: "session = %#", session)
let sortDescriptors = [SortDescriptor(\Player.timestamp)] // need something to sort by.
_players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
}
That acts like a filter and will only return the players that have the session relation equalling that object. The reason you have to fetch like this is so any changes will be detected.
The second problem about the bindings can be solved like this:
struct PlayerView: View{
#ObservedObject var player: Player {
var body:some View {
if let score = Binding($player.score) {
TextField("Score", score)
}else{
Text("Player score missing")
}
}
}
This View takes the player object as an ObservedObject so body will be called when any of its properties change and allows you to get a binding to property. The Binding init takes an optional binding and returns a non-optional, allowing you to use it with a TextField.
Are there any better syntax for generics? Belowed code gives me huge ugly generics definition in the beginning of view.
struct ViewUserSettings<TypeGif: ModelGifProtocol, TypeFont: ModelFontProtocol, TypeMagicText: ModelMagicTextProtocol, TypePreSetAnswer: ModelPreSetAnswerProtocol>: View where TypeGif: Hashable, TypeFont: Hashable, TypeMagicText: Hashable, TypePreSetAnswer: Hashable
I had to define Hashable inside generics otherwise compiler gives Error when I tried to use this Protocol in SwiftUI ForEach.
ModelGif.swift
// MARK: - Protocol
enum GifType: String, Codable {
case fooA = "fooA more"
case fooB = "fooB more"
case fooC = "fooC more"
// MARK: Properties
var fileName600px: String {
switch self {
case .fooA: return "fooAHQ"
case .fooB: return "fooBHQ"
case .fooC: return "fooCHQ"
}
}
var fileName80px: String {
switch self {
case .fooA: return "fooALQ"
case .fooB: return "fooBLQ"
case .fooC: return "fooCLQ"
}
}
}
protocol ModelGifProtocol: Codable {
var gif: GifType { get }
}
// MARK: - Module
struct ModelGif: ModelGifProtocol, Hashable {
// MARK: Properties
let gif: GifType
// MARK: Initialization
init(_ gif: GifType) {
self.gif = gif
}
// MARK: Static Properties
static var fooA = Self(.fooA)
static var fooB = Self(.fooB)
static var fooC = Self(.fooC)
}
ModelMagicText.swift
// MARK: - Protocol
protocol ModelMagicTextProtocol: Codable {
var id: UUID { get }
var context: String { get }
init(_ context: String, id: UUID) throws
init(_ context: String) throws
}
// MARK: - Module
struct ModelMagicText: ModelMagicTextProtocol, Hashable {
// MARK: Properties
let id: UUID
let context: String
// MARK: Initialization
init(_ context: String, id: UUID) throws {
guard context.count <= 30 else { throw ContextError.tooLong } // restricted at ViewAnimatingCircleText.modifyDynamicText()
self.context = context.uppercased()
self.id = id
}
init(_ context: String) throws {
try self.init(context, id: UUID())
}
init(_ model: ModelMagicTextProtocol) throws {
try self.init(model.context, id: model.id)
}
// MARK: Static Properties
static let defaultText = try! ModelMagicText("foooooooooooo")
}
// MARK: - Extension: ContextError
extension ModelMagicText {
enum ContextError: Error {
case tooLong
}
}
We can format that more readably using multiple lines. We can also move the Hashable constraints into the generic parameter list:
struct ViewUserSettings<
TypeGif: ModelGifProtocol & Hashable,
TypeFont: ModelFontProtocol & Hashable,
TypeMagicText: ModelMagicTextProtocol & Hashable,
TypePreSetAnswer: ModelPreSetAnswerProtocol & Hashable
>: View {
var gif: TypeGif
var font: TypeFont
var magicText: TypeMagicText
var preSetAnswer: TypePreSetAnswer
var body: some View { EmptyView() }
}
You only need a where clause when you have equality constraints, but your example code doesn't have any equality constraints.
If you change each of your model protocols to inherit from Hashable, you can eliminate the Hashable constraints entirely:
protocol ModelGifProtocol: Hashable { }
protocol ModelFontProtocol: Hashable { }
protocol ModelMagicTextProtocol: Hashable { }
protocol ModelPreSetAnswerProtocol: Hashable { }
struct ViewUserSettings<
TypeGif: ModelGifProtocol,
TypeFont: ModelFontProtocol,
TypeMagicText: ModelMagicTextProtocol,
TypePreSetAnswer: ModelPreSetAnswerProtocol
>: View {
...
There's also the question of whether you need such verbose names. Do you really need the Type and Model prefixes?
protocol GifProtocol: Hashable { }
protocol FontProtocol: Hashable { }
protocol MagicTextProtocol: Hashable { }
protocol PreSetAnswerProtocol: Hashable { }
struct ViewUserSettings<
Gif: GifProtocol,
Font: FontProtocol,
MagicText: MagicTextProtocol,
PreSetAnswer: PreSetAnswerProtocol
>: View {
var gif: Gif
var font: Font
var magicText: MagicText
var preSetAnswer: PreSetAnswer
var body: some View { EmptyView() }
}
Keep in mind that you can still refer to SwiftUI's Font type as SwiftUI.Font inside ViewUserSettings.
You didn't give any details regarding how your protocols are defined. Do you really need your own ModelFontProtocol? SwiftUI's Font is already Hashable, so if you can just use the concrete Font type, your code will be simpler. Maybe you can use concrete types in place of your other protocols too, but we can't say without seeing how they're defined.
If you're using the same set of type parameters and constraints for multiple views, there are ways to factor the common stuff out. You should edit your question to include more example code if that's the case.
To provide some context, Im writing an order tracking section of our app, which reloads the order status from the server every so-often. The UI on-screen is developed in SwiftUI. I require an optional image on screen that changes as the order progresses through the statuses.
When I try the following everything works...
My viewModel is an ObservableObject:
internal class MyAccountOrderViewModel: ObservableObject {
This has a published property:
#Published internal var graphicURL: URL = Bundle.main.url(forResource: "tracking_STAGEONE", withExtension: "gif")!
In SwiftUI use the property as follows:
GIFViewer(imageURL: $viewModel.graphicURL)
My issue is that the graphicURL property has a potentially incorrect placeholder value, and my requirements were that it was optional. Changing the published property to: #Published internal var graphicURL: URL? causes an issue for my GIFViewer which rightly does not accept an optional URL:
Cannot convert value of type 'Binding<URL?>' to expected argument type 'Binding<URL>'
Attempting the obvious unwrapping of graphicURL produces this error:
Cannot force unwrap value of non-optional type 'Binding<URL?>'
What is the right way to make this work? I don't want to have to put a value in the property, and check if the property equals placeholder value (Ie treat that as if it was nil), or assume the property is always non-nil and unsafely force unwrap it somehow.
Below is an extension of Binding you can use to convert a type like Binding<Int?> to Binding<Int>?. In your case, it would be URL instead of Int, but this extension is generic so will work with any Binding:
extension Binding {
func optionalBinding<T>() -> Binding<T>? where T? == Value {
if let wrappedValue = wrappedValue {
return Binding<T>(
get: { wrappedValue },
set: { self.wrappedValue = $0 }
)
} else {
return nil
}
}
}
With example view:
struct ContentView: View {
#StateObject private var model = MyModel()
var body: some View {
VStack(spacing: 30) {
Button("Toggle if nil") {
if model.counter == nil {
model.counter = 0
} else {
model.counter = nil
}
}
if let binding = $model.counter.optionalBinding() {
Stepper(String(binding.wrappedValue), value: binding)
} else {
Text("Counter is nil")
}
}
}
}
class MyModel: ObservableObject {
#Published var counter: Int?
}
Result:
I'm curious about the default implementation of AnyView in SwiftUI. How to put structs with different generic types into a protocol array?
For example:
let a = AnyView(Text("hello"))
let b = AnyView(Image(systemName: "1.circle"))
let genericViews = [a, b] // No compile error
And my implementation:
struct TypeErasedView<V: View>: View {
private var _view: V
init(_ view: V) {
_view = view
}
var body: V {
_view
}
}
let a = TypeErasedView(Text("Hello"))
let b = TypeErasedView(Image(systemName: "1.circle"))
let genericViews = [a, b] // compile error
The compile error will be "Heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional".
Does anyone have any ideas?
Here is a demo of possible approach. It is simplified, but shows the generic idea of how this might be done... or at least a direction.
Full compilable & working module. Tested on Xcode 11.2 / iOS 13.2
import SwiftUI
private protocol TypeErasing {
var view: Any { get }
}
private struct TypeEraser<V: View>: TypeErasing {
let orinal: V
var view: Any {
return self.orinal
}
}
public struct MyAnyView : View {
public var body: Never {
get {
fatalError("Unsupported - don't call this")
}
}
private var eraser: TypeErasing
public init<V>(_ view: V) where V : View {
eraser = TypeEraser(orinal: view)
}
fileprivate var wrappedView: Any { // << they might have here something specific
eraser.view
}
public typealias Body = Never
}
struct DemoAnyView: View {
let container: [MyAnyView]
init() {
let a = MyAnyView(Text("Hello"))
let b = MyAnyView(Image(systemName: "1.circle"))
container = [a, b]
}
var body: some View {
VStack {
// dynamically restoring types is different question and might be
// dependent on Apple's internal implementation, but here is
// just a demo that it works
container[0].wrappedView as! Text
container[1].wrappedView as! Image
}
}
}
struct DemoAnyView_Previews: PreviewProvider {
static var previews: some View {
DemoAnyView()
}
}
It's because there's a generic constraint on yours. AnyView has no generic constraint. You instantiate it with an underlying generic View, but its Body is always declared as Never. There might be compiler magic happening here as I couldn't get a generic constraint-less version to work.
I've created a SwiftUI Bindable class with two properties, like
public class Clock: BindableObject {
public let didChange = PassthroughSubject<Clock, Never>()
public var time: Date = Date() {
didSet { didChange.send(self) }
}
public var useMilitaryTime = false {
didSet { didChange.send(self) }
}
}
This works exactly as expected. But when I try to add a property with a custom type (either as a struct, a class, or a tuple), it fails to compile:
public var sunriseSunset = (Date(), Date()) {
didSet { didChange(self) }
}
saying I "Cannot call value of non-function type 'PassthroughSubject'". It also fails the same way if I try
public var sunriseSunset: (Date, Date)? = nil {
didSet { didChange(self) }
}
The Diagnostics button is unhelpful; it just says that it "Failed to build active schema."
What types can be used as properties of a Bindable class?
You are calling didChange(self) which causes the error. Call didChange.send(self) instead.