Cannot Convert From One Binding to Another - swiftui

I'm developing a screen in SwiftUI and have the following code:
...
#EnvironmentObject var externalState: MainStateObject
...
SelectOptionPopover(options: $externalState.depots,
selectedOption: selectedDepot,
prompt: "Depot: ")
...
SelectOptionPopover is a view that I created to handle a variety of popovers. For the options, it expects an array of [SelectOptionPopoverOption], which is declared like this:
protocol SelectOptionPopoverOption {
var displayName: String { get }
}
Now, the issue I have is that when I pass an array of SelectOptionPopoverOptions, it works just fine. But if I pass an array of another type that conforms to SelectOptionPopoverOptions, the conversion fails with something like:
'Binding<[Depot]>' is not convertible to 'Binding<[SelectOptionPopoverOption]>'
These may be the exact same objects, but work when they're identified as SelectOptionPopoverOptions but not when identified as a Depots.
I can work around this by using arrays of SelectedOptionPopoverOption and casting them as needed, but it would sure be cleaner to be able to use the conforming types instead.
Any ideas on how I could use the more specific types instead?

You can declare and adopt your custom SelectOptionPopover view as generics
struct SelectOptionPopover<T>: View where T: SelectOptionPopoverOption {
#Binding var options: [T]
...

Related

SwiftUI and Combine

I'm following a video on the Firebase YouTube channel. Starting around 27:45, the instructor is trying to set a variable based on a Boolean and ends up with the following code in init(task: Task):
$task
.map { task in
task.isCompleted ? "checkmark.circle.fill" : "circle"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables)
This seems overly convoluted to me. First, I can't find documentation on using .map on a struct object, only on arrays, etc. Second, what is with this &cancellables thing? (It's defined as private var cancellables = Set<AnyCancellable>() before the init{}.) Third, why all this code, and not simply:
task.completionStateIconName = task.isCompleted ? "checkmark.circle.fill" : "circle"
This seems to give the same result, but will there be something down the line that the first code fragment works, but the second doesn't?
$task (with the $ prefix) is a projected value of the #Published property wrapper, and it returns a variable of the type Published.Publisher. In other words, its a Combine publisher, which publishes a value whenever the property - in this case Task - changes.
If you didn't learn about the Combine framework (or other reactive frameworks), this answer is definitely not going to be enough. At a high-level, a Combine publisher emits values, which you can transform through operators like .map, and eventually subscribe to, for example with .sink or .assign.
So, line-by-line:
// a publisher of Task values
$task
// next, transform Task into a String using its isCompleted property
.map { task in
task.isCompleted ? "circle.fill" : "circle"
}
// subscribe, by assigning the String value to the completionStateIconName prop
.assign(to: \.completionStateIconName, on: self)
Now, the above returns an instance of AnyCancellable, which you need to retain while you want to receive the values. So you either need to store it directly as a property, or use .store to add it to a Set<AnyCancellable> - a common approach.
So, why is it so convoluted? This is, presumably, built so that if task property ever changes, the Combine pipeline would update the completionStateIconName property.
If you just did:
completionStateIconName = task.isCompleted ? "circle.fill" : "circle"
that would assign the value just in the beginning.
That being said, in this particular case it might actually be unnecessarily too convoluted to use Combine, whereas just using didSet:
var task: Task {
didSet {
completionStateIconName ? task.isCompleted ? "circle.fill" : "circle"
}
}

How to increment a published var when accessed

I'm trying to setup some kind of global counter in my app.
What the best way of doing this?
I have the following code that works but it's damn ugly, I'd like to increment when the var is accessed so I don't have to call the function.
I tried to use get and set but it's not possible with property wrappers.
class GlobalDisplayInfo: ObservableObject {
#Published var nextAvailableInt : Int = 0
func getNextAvailableInt() -> Int {
nextAvailableInt += 1
return self.nextAvailableInt
}
}
What you want to achieve is not possible as it is against how Published works.
Let's say you access the published var in two places:
When the first one accesses the var, it would increment the var which would require the second place where it uses this var to refresh (access to this var again) which would require the var to increment again which would require the other place which uses this var to access and it just goes on - I think you got the point.
Your problem basically sounds rather like a design problem and I suggest you review it. Also, I think it'd be better if you just tell us what you want to achieve (without using any coding language, just a plain explanation of what you want) then an answer might come up.
It is not clear the usage, but looks like you need the following
class GlobalDisplayInfo: ObservableObject {
private var _nextAvailableInt = 0
var nextAvailableInt : Int {
_nextAvailableInt += 1
return _nextAvailableInt
}
}

What is the best way to demux a publisher's Output?

I have an ObservableObject with a few publishers:
private class ViewModel: ObservableObject {
#Published var top3: [SearchResult] = []
#Published var albums: [SearchResult.Album] = []
#Published var artists: [SearchResult.Artist] = []
}
The endpoint is a URLSessionDataPublisher that sends a single collection of values that can be either an album or an artist (there are actually more types but I'm reducing the problem set here.) What is the best way in Combine to separate this collection out into 3 collections: [Album], [Artist], and an array of 3 results that can be either Artist or Album?
DatabaseRequest.Search(for: searchTerm)
.publisher()
// now i want to separate the collection out into [Album] and [Artist] and assign to my 3 #Published vars
.receive(on: DispatchQueue.main)
.sink { }
.store(in: bag)
You are hitting a bit of a (common) fallacy that Combine is responsible for passing the changed data in SwiftUI. It isn't. The only thing Combine is doing here is providing the content-less message that some data has changed, and then the SwiftUI components that are using the relevant model object go and look for their data.
The data transfer in SwiftUI is entirely using Binding, which are essentially get and set closures under the covers.
So you don't really need to worry about demuxing a combine stream - and there isn't one that has "one" of these kinds of data in it. Combine would have trouble with that since it's strongly typed for both Output type and Failure type.
There's a bit more written about this in Using Combine under the chapter
SwiftUI and Combine (chapter link to the free HTML version)

kotlin synthetic Integer cannot be cast to floating button

Why does
val fabOpen = view.findViewById(R.id.fab_open) as FloatingActionButton
work correctly and not error out but the kotlin synthetic of
val fabOpen = R.id.fab_open as FloatingActionButton
gives me the
java.lang.Integer cannot be cast to android.support.design.widget.FloatingActionButton
error? They both show that they are casting as FloatingActionButton. Using the synthetics is not only less code, it's better memory management and I'd prefer to do it this way. Is there something I am missing?
****Update**** I forgot to mention I am trying access the FloatingActionButton inside of a fragment if that makes a difference.
R.id.fab_open is a generated integer value that will be set as the ID of your button at inflation, and that you can look up with findViewById like you've shown.
Casting this to a button won't work, think (FloatingActionButton) 2688664731 in Java terms.
If you wish to use Kotlin Android Extensions and its synthetic properties, those are simply named as the ID itself, but they don't come from the R class - and you don't need to assign them to variables or properties. You can simply use your button like this:
fab_open.setOnClickListener { ... }
fab_open.visibility = View.VISIBLE
I figured out the issue.
to access an element in a view using kotlin synthetic it looks like this:
import kotlinx.android.synthetic.main.chatter_main.view.*
class ChatterMain : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.chatter_main, container, false)
return view
}
Above I imported the kotlin synthetic of the view, inflated the view, and
then I accessed the ID of the element like this:
view.fab_open.visibility = GONE or view.fab_open.SetOnClickListener {}
With the Kotlin Synthetic, you don't need to use:
val fabOpen = R.id.fab_open as FloatingActionButton so I removed that statement.

How to store array of strings into Realm instance using a Dictionary?

I am new to Realm and having this issue.
I am having a Dictionary like this
{
firstName : "Mohshin"
lastName : "Shah"
nickNames : ["John","2","3","4"]
}
and a class like this
class User: Object {
var firstName: String?
var lastName: String?
var nickNames: [String]?
}
While I am trying to insert values it is throwing an exception as below
Property 'nickNames' is declared as 'NSArray', which is not a supported RLMObject property type. All properties must be primitives, NSString, NSDate, NSData, NSNumber, RLMArray, RLMLinkingObjects, or subclasses of RLMObject.
See https://realm.io/docs/objc/latest/api/Classes/RLMObject.html for more information.
I have also tried
var nickNames = NSArray()
var nickNames = NSMutableArray()
But not working.Do I need to make the Nickname model class and create a property as follow or there's a way to do this ?
var nickNames = List<Nickname>()
UPDATE:
You can now store primitive types or their nullable counterparts (more specifically: booleans, integer and floating-point number types, strings, dates, and data) directly within RLMArrays or Lists. If you want to define a list of such primitive values you no longer need to define cumbersome single-field wrapper objects. Instead, you can just store the primitive values themselves.
Lists of primitive values work much the same way as lists containing objects, as the example below demonstrates for Swift:
class Student : Object {
#objc dynamic var name: String = ""
let testScores = List<Int>()
}
// Retrieve a student.
let realm = try! Realm()
let bob = realm.objects(Student.self).filter("name = 'Bob'").first!
// Give him a few test scores, and then print his average score.
try! realm.write {
bob.testScores.removeAll()
bob.testScores.append(94)
bob.testScores.append(89)
bob.testScores.append(96)
}
print("\(bob.testScores.average()!)") // 93.0
All other languages supported by Realm also supports lists of primitive types.
you can find more details here. https://academy.realm.io/posts/realm-list-new-superpowers-array-primitives/
class BlogPost: Object {
#objc var title = ""
let tags = List<String>()
convenience init(title: String, tag: String) {
self.init()
self.title = title
self.tags.append(tag)
}
}
more details here
Realm doesn't support model properties that are NSArrays, and currently doesn't support properties that are Lists of primitive types (such as Lists of strings). For now, you should create a Nickname model that wraps the nickname string, and then store a List<Nickname>, as in your sample code above.
This ticket on our GitHub repository tracks support for lists of primitives, although none of the comments from 2014 are particularly relevant anymore. You can follow that ticket if you want to be informed as to when that feature will become available.
(Also note that you should declare your list property as let, not var.)
Using List is pretty much the only way to do it. When you initialize the Nickname object (the realm object you created for using in List), you should provide an array for the value param, even if the value is actually just one string. For example:
let aNickName = Nickname(value:["John"])
That is why it was throwing an error saying "Invalid value 'John' to initialize object of type 'Nickname'".