How can I make a Generic PreferenceKey in SwiftUI? - swift

I want make a Generic PreferenceKey, which I got 2 issues with my codes.
First I need define () for T, for using normal type like String, Int as String() or Int() so I need memberwise initializer for T.
Second Xcode complain that my PreferenceKey does not conform to Equatable, while I did it! <T: Equatable>
How can I solve this 2 issues? thanks
struct ContentView: View {
#State private var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.preference(key: CustomPreferenceKey.self, value: stringOfText)
.onPreferenceChange(CustomPreferenceKey.self) { newValue in print(newValue) }
}
}
struct CustomPreferenceKey<T: Equatable>: PreferenceKey {
static var defaultValue: T { get { return T() } }
static func reduce(value: inout T, nextValue: () -> T) { value = nextValue() }
}

Here is a possible approach to move with. Tested as worked with Xcode 12.4 / iOS 14.4
protocol Initable {
init()
}
extension String: Initable {
}
struct CustomPreferenceKey<T: Equatable & Initable>: PreferenceKey {
typealias Value = T
static var defaultValue: T { get { T() } }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct ContentView: View {
#State private var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.preference(key: CustomPreferenceKey<String>.self, value: stringOfText)
.onPreferenceChange(CustomPreferenceKey<String>.self) { newValue in print(newValue) }
}
}

Related

any Identifiable can't conform to 'Identifiable'

update: add same error about Hashable
I have created an Identifiable compliant protocol and compliant structures. Then, when I create the list and reference it in ForEach, I get the error Type 'any TestProtocol' cannot conform to 'Identifiable'(I get the same error about Hashable).
How should I fix this program?
If I write ForEach(list, id: \.id) , it works, but I don't think it makes sense to be Identifiable compliant.
import SwiftUI
protocol TestProtocol: Identifiable, Hashable {
var id: UUID { get set }
var name: String { get set }
func greeting() -> String
static func == (lhs: Self, rhs: Self) -> Bool
}
extension TestProtocol {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
}
struct Person: TestProtocol {
var id = UUID()
var name: String
func greeting() -> String {
return "my name is \(name) and I'm a human."
}
}
struct Dog: TestProtocol {
var id = UUID()
var name: String
func greeting() -> String {
return "my name is \(name) and I'm a dog."
}
}
struct ContentView: View {
var list: [any TestProtocol] = [Person(name: "p1"), Dog(name: "d1")]
#State var selected: any TestProtocol
var body: some View {
VStack {
Picker(selection: $selected) { // Type 'any TestProtocol' cannot conform to 'Hashable'
ForEach(list) { l in // Type 'any TestProtocol' cannot conform to 'Identifiable'
Text(l.greeting()).tag(l) // Type 'any TestProtocol' cannot conform to 'Hashable'
}
} label: {
Text("select")
}
}
}
}
Your error message complaining about Hashable is a "red hering". The protocol TestProtocol, and therefor all structs conforming to it, conforms to Hashable.
let person = Person(name: "IAmHashable")
print(person.hashValue)
The reason this is failing is within the Picker. It needs a concrete type and not a protocol. One solution would be to create a "Container" type and a custom binding that handles this.
struct Container: Identifiable, Hashable{
//implement the same equality as in your TestProtocol
static func == (lhs: Container, rhs: Container) -> Bool {
rhs.wrapped.id == lhs.wrapped.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(wrapped)
}
var wrapped: any TestProtocol
//for convenience
var id: UUID {wrapped.id}
}
and the ContentView:
struct ContentView: View {
let startArr: [any TestProtocol] = [Person(name: "p1"), Dog(name: "d1")]
#State private var selected: (any TestProtocol)?
var body: some View {
// list of wrapped protocols
var list: [Container] = { startArr.map{Container(wrapped: $0)}}()
// binding
let selectionBinding: Binding<Container> = .init {
let returninstance = list.first { cont in
cont.id == selected?.id
}
return returninstance ?? list[0]
} set: { container in
selected = container.wrapped
}
// viewCode
VStack {
Picker(selection: selectionBinding) {
ForEach(list) { l in
Text(l.wrapped.greeting())
.tag(l)
}
} label: {
Text("select")
}
// confirmation selection changed
Text(selected?.name ?? "no Selection")
}
}
}
Remarks:
This solution has a few drawbacks:
your initial array startArr should never be empty, else return returninstance ?? list[0] will break you code. This can be handled, but I think this is out of the scope of this question.
the equality comparison of Container needs to be the same as in TestProtocol as you cannot compare two any Protocols
on the appearance of ContainerView selected will be nil until something is selected. Any usage, e.g.: the Text element in this case, needs to deal with this. But you could probably set this in .onApear.

Let swift #Binding accept any value conforming to `BinaryFloatingPoint`?

I have the following code:
class MyModel:ObservableObject {
#Published var time:Double = 0
}
struct ContentView: View {
#StateObject var model = MyModel()
#State var someValue:Float = 0
var body: some View {
TView(value: $model.time)
}
}
struct TView: View {
#Binding var value:Float
var body: some View {
Text("value: \(value)")
}
}
Obvisouly that code cannot work because the binding wants a Float and model.time is a Double. Error: Cannot convert value of type 'Binding<Double>' to expected argument type 'Binding<Float>'.
I'd like to mimic what Slider does, where it can bind to values as long as they conform to BinaryFloatingPoint.
I looked at Swift.Math.Floating and Double does conform to BinaryFloatingPoint.
...
extension Double : BinaryFloatingPoint {
...
Looking at the Slider init:
init<V>(value: Binding<V>, in bounds: ClosedRange<V> = 0...1, onEditingChanged: #escaping (Bool) -> Void = { _ in }) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint
How can I change TView, so that it can bind to any type conforming to BinaryFloatingPoint.
My objective is to do what Slider does, allowing me to pass in a Double, Float, etc...
I tried to change TView to:
struct TView: View {
#Binding var value:Float
init<V>(theValue:Binding<V>) where V : BinaryFloatingPoint {
self._value = theValue
}
var body: some View {
Text("value: \(value)")
}
}
But, it errors: Cannot assign value of type 'Binding<V>' to type 'Binding<Float>'
How can I change things so that TView can bind to any value conforming to BinaryFloatingPoint?
Move generics to view declaration level, like
struct TView<V: BinaryFloatingPoint>: View { // << here !!
#Binding var value:V // << here !!
init(value:Binding<V>) { // << here !!
self._value = value
}
var body: some View {
Text("value: \(Double(value))")
}
}
Tested with Xcode 13 / iOS 15

How can I make a function which returns some View and accepts a closure in SwiftUI?

I want build a function for View which accepts a closure like onChange or onPreferenceChange here is my code, with this code I can read value or the change of value, but I cannot send it back to my ContentView, because I want use print(newValue) in ContentView. How can I do this? thanks for your help and time.
import SwiftUI
struct ContentView: View {
#State private var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.perform(value: stringOfText) // <<: Here: I want get back data as: { newValue in print(newValue) }
Button("update") { stringOfText += " updated!" }.padding()
}
}
extension View {
func perform(value: String) -> some View {
return self
.preference(key: CustomPreferenceKey.self, value: value)
.onPreferenceChange(CustomPreferenceKey.self) { newValue in
print(newValue)
// I want send back newValue to ContentView that perform used!
}
}
}
struct CustomPreferenceKey: PreferenceKey {
static var defaultValue: String { get { return String() } }
static func reduce(value: inout String, nextValue: () -> String) { value = nextValue() }
}
so we only need to add callback closure parameter in perform function. like this
func perform(value: String,callback : #escaping (_ value : String ) -> Void) -> some View
so now we have a callback closure in which we can send the value back.
struct ContentView: View {
#State private var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.perform(value: stringOfText) {
newValue in
print(newValue)
}
Button("update") { stringOfText += " updated!" }.padding()
}
}
extension View {
func perform(value: String,callback : #escaping (_ value : String ) -> Void) -> some View {
return self
.preference(key: CustomPreferenceKey.self, value: value)
.onPreferenceChange(CustomPreferenceKey.self) { newValue in
callback(newValue)
}
}
}
struct CustomPreferenceKey: PreferenceKey {
static var defaultValue: String { get { return String() } }
static func reduce(value: inout String, nextValue: () -> String) { value = nextValue() }
}

I need to refresh SwiftUI View, when #propertyWrapper is passed into

I have an example struct:
public struct Axis: Hashable, CustomStringConvertible {
public var name: String
public var description: String {
return "Axis: \"\(name)\""
}
}
And property wrapper to make some operations on [Axis] struct.
#propertyWrapper
struct WrappedAxes {
var wrappedValue: [Axis] {
// This is just example, in real world it's much more complicated.
didSet {
for index in wrappedValue.indices {
var elems = Array(wrappedValue[index].name.split(separator: " "))
if elems.count>1 {
elems.removeLast()
}
let new = elems.reduce(into:"", {$0 += "\($1) "})
wrappedValue[index].name = new+("\(Date())")
} } } }
And I try to add, insert and remove Axes in SwiftUI View:
public struct ContentView: View {
#Binding var axes: [Axis]
public var body: some View {
VStack {
ForEach(axes.indices, id:\.self) {index in
HStack {
TextField("", text: $axes[index].name)
Button("Delete", action: {deleteAxis(index)})
Button("Insert", action: {insertAxis(index)})
}
}
Button("Add", action: addAxis)
}
}
var addAxis: () -> Void {
return {
axes.append(Axis(name: "New"))
print (axes)
}
}
var deleteAxis: (_:Int)->Void {
return {
if $0 < axes.count {
axes.remove(at: $0)
}
print (axes)
}
}
var insertAxis: (_:Int)->Void {
return {
if $0 < axes.count {
axes.insert(Axis(name: "Inserted"), at: $0)
}
print (axes)
}
}
public init (axes: Binding<[Axis]>) {
self._axes = axes
}
}
As far, as print (axes) shows changes are made, View never updates. I made very small App to test in which I call ContentView:
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
#WrappedAxes var axes = [Axis(name: "FirstOne")]
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(
axes: Binding (
get: {self.axes},
set: { [self] in axes = $0}))
.... // No fancy stuff
I'm open for all critique of code itself, and help: how to push this view (and all possible future subviews) to update when axes changed?
The thing is that #Binding is for nested Views. When you want to make changes in a SwiftUI view, you have to use the #State instead in the one where the action starts. Plus, you don't need to set an initialiser this way. You can set your value like this, inside an ObservableObject to handle your logic:
struct Axis {
var name: String
var description: String {
return "Axis: \"\(name)\""
}
}
final class AxisViewModel: ObservableObject {
#Published var axes: [Axis] = [Axis(name: "First")]
init() { handleAxes() }
func addAxis() {
axes.append(Axis(name: "New"))
handleAxes()
}
func insertAxis(at index: Int) {
axes.insert(Axis(name: "Inserted"), at: index)
handleAxes()
}
func handleAxes() {
for index in axes.indices {
var elems = Array(axes[index].name.split(separator: " "))
if elems.count > 1 {
elems.removeLast()
}
let new = elems.reduce(into:"", { $0 += "\($1) " })
axes[index].name = new + ("\(Date())")
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = AxisViewModel()
var body: some View {
VStack {
ForEach(viewModel.axes.indices, id:\.self) { index in
HStack {
TextField("", text: $viewModel.axes[index].name)
Button("Insert", action: { viewModel.insertAxis(at: index) })
}
}
Button("Add", action: viewModel.addAxis)
}
}
}
I made a new struct Axes:
public struct Axes {
#WrappedAxes var _axes: [Axis]
public subscript (index: Int) -> Axis {
get { return _axes[index]}
set { _axes[index] = newValue}
}
// and all needed functions and vars to simulate Array behaviour:
public mutating func append(_ newElement: Axis ) {
_axes.append(newElement)
}
public mutating func insert(_ newElement: Axis, at index: Int ) {
_axes.insert(newElement, at: index)
}
public mutating func remove(at index: Int ) {
_axes.remove(at: index)
}
....
}
and put it into #ObservalbeObject:
public class Globals: ObservableObject {
#Published public var axes: Axes
....
}
Then defined AxesView as
public struct AxesView: View {
#ObservedObject var globals: Globals
public var body: some View {
...
}
...
}
That's all. For a while it works.

Swift: how to make my custom struct Node<T> conform to Hashable?

Node is a generic type.
struct Node<T: Hashable>: Hashable {
var label: T
init(_ label: T) {
self.label = label
}
var hashValue : Int {
get {
return label.hashValue
}
}
}
extension Node : Equatable {}
// MARK: Equatable
func ==<T>(lhs: Node<T>, rhs: Node<T>) -> Bool {
return lhs.label == rhs.label
}
But it doesn't work when I try the following:
let nodes = Set<Node<String>>()
The compiler complains that Node<String> doesn't conform to Hashable. How to make Node<String> conform to Hashable?
You have to implement the == method as part of the Equatable protocol for your struct as well:
func ==<T, K>(lhs:Node<T>, rhs:Node<K>) -> Bool {
return lhs.hashValue == rhs.hashValue
}
The reason for that is Hashable inherits from Equatable.
The following is a complete working playground example:
struct Node<T: Hashable> : Hashable {
var label: T
init(_ label: T) {
self.label = label
}
var hashValue : Int {
get {
return label.hashValue
}
}
}
func ==<T>(lhs:Node<T>, rhs:Node<T>) -> Bool {
return lhs.hashValue == rhs.hashValue
}
var nodes = Set<Node<String>>()
nodes.insert(Node("hi"))
nodes.insert(Node("ho"))
nodes.insert(Node("hi"))