Swift Combine: Separate published array and then assign - swift

I have a publisher which returns an array of RetailStoreSlotDay objects. I need to separate these out based on a certain property and then assign to separate publishers within the view model.
So, my publisher is:
#Published var selectedDaySlot: RetailStoreSlotDay?
Within the RetailStoreSlotDay object I have a property called 'daytime' which is set to either:
"morning"
"afternoon"
"evening"
I then have these separate publishers that I need to assign values to when the selectedDaySlot is amended:
#Published var morningTimeSlots = [RetailStoreSlotDayTimeSlot]()
#Published var afternoonTimeSlots = [RetailStoreSlotDayTimeSlot]()
#Published var eveningTimeSlots = [RetailStoreSlotDayTimeSlot]()
At the moment, I have the following subscription set up and declared in the init of the view model:
private func setupDeliveryDaytimeSectionSlots() {
$selectedDaySlot
.map { timeSlot in
return timeSlot?.slots
}
.replaceNil(with: [])
.sink(receiveValue: { slots in
self.morningTimeSlots = slots.filter { $0.daytime == "morning" }
self.afternoonTimeSlots = slots.filter { $0.daytime == "afternoon" }
self.eveningTimeSlots = slots.filter { $0.daytime == "evening" }
})
.store(in: &cancellables)
}
This works fine, but I'm sure there must be an operator which will perform this in a more sophisticated way, whereby I can assign the values without using sink. Wondering if there is a better way around this.

You could group these slots into a dictionary, using Dictionary(grouping:by:):
let dictionaryPublisher = $selectedDaySlot
.map { timeSlot in
Dictionary(grouping: timeSlot?.slots ?? [], by: \.daytime)
}
Then you can assign the values associated with the different keys of the dictionary to the different properties:
dictionaryPublisher.map { $0["morning"] ?? [] }.assign(to: &self.$morningTimeSlots)
dictionaryPublisher.map { $0["afternoon"] ?? [] }.assign(to: &self.$afternoonTimeSlots)
dictionaryPublisher.map { $0["evening"] ?? [] }.assign(to: &self.$eveningTimeSlots)
Rather than using strings as the dayTime values, consider using an enum instead:
enum TimeOfDay: Hashable {
case morning, afternoon, evening
}

Related

Change parameter of an object if list has a specific key [duplicate]

I am still not sure about the rules of struct copy or reference.
I want to mutate a struct object while iterating on it from an array:
For instance in this case I would like to change the background color
but the compiler is yelling at me
struct Options {
var backgroundColor = UIColor.blackColor()
}
var arrayOfMyStruct = [MyStruct]
...
for obj in arrayOfMyStruct {
obj.backgroundColor = UIColor.redColor() // ! get an error
}
struct are value types, thus in the for loop you are dealing with a copy.
Just as a test you might try this:
Swift 3:
struct Options {
var backgroundColor = UIColor.black
}
var arrayOfMyStruct = [Options]()
for (index, _) in arrayOfMyStruct.enumerated() {
arrayOfMyStruct[index].backgroundColor = UIColor.red
}
Swift 2:
struct Options {
var backgroundColor = UIColor.blackColor()
}
var arrayOfMyStruct = [Options]()
for (index, _) in enumerate(arrayOfMyStruct) {
arrayOfMyStruct[index].backgroundColor = UIColor.redColor()
}
Here you just enumerate the index, and access directly the value stored in the array.
Hope this helps.
You can use use Array.indices:
for index in arrayOfMyStruct.indices {
arrayOfMyStruct[index].backgroundColor = UIColor.red
}
You are working with struct objects which are copied to local variable when using for in loop. Also array is a struct object, so if you want to mutate all members of the array, you have to create modified copy of original array filled by modified copies of original objects.
arrayOfMyStruct = arrayOfMyStruct.map { obj in
var obj = obj
obj.backgroundColor = .red
return obj
}
It can be simplified by adding this Array extension.
Swift 4
extension Array {
mutating func mutateEach(by transform: (inout Element) throws -> Void) rethrows {
self = try map { el in
var el = el
try transform(&el)
return el
}
}
}
Usage
arrayOfMyStruct.mutateEach { obj in
obj.backgroundColor = .red
}
For Swift 3, use the enumerated() method.
For example:
for (index, _) in arrayOfMyStruct.enumerated() {
arrayOfMyStruct[index].backgroundColor = UIColor.redColor()
}
The tuple also includes a copy of the object, so you could use for (index, object) instead to get to the object directly, but since it's a copy you would not be able to mutate the array in this way, and should use the index to do so. To directly quote the documentation:
If you need the integer index of each item as well as its value, use
the enumerated() method to iterate over the array instead. For each
item in the array, the enumerated() method returns a tuple composed of
an integer and the item.
Another way not to write subscript expression every time.
struct Options {
var backgroundColor = UIColor.black
}
var arrayOfMyStruct = [Options(), Options(), Options()]
for index in arrayOfMyStruct.indices {
var option: Options {
get { arrayOfMyStruct[index] }
set { arrayOfMyStruct[index] = newValue }
}
option.backgroundColor = .red
}
I saw this method in some code and it seems to be working
for (var mutableStruct) in arrayOfMyStruct {
mutableStruct.backgroundColor = UIColor.redColor()
}

Swift Combine, how to combine publishers and sink when only one publisher's value changes?

I have found ways of combining publishers using MergeMany or CombineLatest, but I don't seem to find a solution in my particular case.
Example:
class Test {
#Published var firstNameValid: Bool = false
#Published var lastNameValid: Bool = false
#Published var emailValid: Bool = false
#Published var allValid: Bool = false
}
I want allValid to become false when any of the previous publishers are set to false, and true if all of them are true.
I also don't want to hardcode the list of publishers I am observing since I want a flexible solution, so I want to be able to pass an array of Bool publishers to whatever code I use to do this.
I tried this
let fieldPublishers = [$firstNameValid, $lastNameValid, $emailValid]
Publishers
.MergeMany(fieldPublishers)
.sink { [weak self] values in
self?.allValid = values.allSatisfy { $0 }
}
.store(in: &subscribers)
But this of course doesn't work because I get an array of publishers and not an array of values. I tried some other ways (forgot which ones) but they only seemed to call sink if I assigned a value during execution to all 3 publishers.
In the case of only using 2 publishers I managed to get it working using CombineLatest.
So the question is: Can I have sink triggered when only one of the publishers in an array changes value after instantiation of Test, and then iterate over the values of all the publishers I am observing?
CombineLatest is indeed correct. You have three publishers so you would use CombineLatest3. In this example, I use CurrentValueSubject publishers instead of #Published, but it's the same principle:
import UIKit
import Combine
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class ViewController: UIViewController {
let b1 = CurrentValueSubject<Bool,Never>(false)
let b2 = CurrentValueSubject<Bool,Never>(false)
let b3 = CurrentValueSubject<Bool,Never>(false)
var storage = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
Publishers.CombineLatest3(b1,b2,b3)
.map { [$0.0, $0.1, $0.2] }
.map { $0.allSatisfy {$0}}
.sink { print($0)}
.store(in: &self.storage)
// false
delay(1) {
self.b1.send(true) // false
delay(1) {
self.b2.send(true) // false
delay(1) {
self.b3.send(true) // true
delay(1) {
self.b1.send(false) // false
delay(1) {
self.b1.send(true) // true
}
}
}
}
}
}
}
Okay, now you may complain, that's okay for a hard-coded three publishers, but I want any number of publishers. Fine! Start with an array of your publishers, accumulate them one at a time with CombineLatest to form a publisher that produces an array of Bool:
let list = [b1, b2, b3] // any number of them can go here!
let pub = list.dropFirst().reduce(into: AnyPublisher(list[0].map{[$0]})) {
res, b in
res = res.combineLatest(b) {
i1, i2 -> [Bool] in
return i1 + [i2]
}.eraseToAnyPublisher()
}
pub
.map { $0.allSatisfy {$0}}
.sink { print($0)}
.store(in: &self.storage)
This is technically not an answer to your last question but wouldn’t the property allValid make more sense as a computed property?
var allValid: Bool {
firstNameValid && lastNameValid && emailValid
}
This would make sure, that allValid at all times represents the logical AND for the other three properties. I hope that I have understood the core of your question and this helped.

How to update interface elements from asynchronous stuff from a singleton

I am developing my first app in SwiftUI and my brain have not wrapped around certain things yet.
I have to display prices, titles and descriptions of inapp purchases on the interface.
I have a singleton model like this, that loads as the app starts.
class Packages:ObservableObject {
enum Package:String, CaseIterable {
typealias RawValue = String
case package1 = "com.example.package1"
case package2 = "com.example.package2"
case package3 = "com.example.package3"
}
struct PurchasedItem {
var productID:String?
var localizedTitle:String = ""
var localizedDescription:String = ""
var localizedPrice:String = ""
}
static let sharedInstance = Packages()
#Published var purchasedItems:[PurchasedItem] = []
func getItem(_ productID:String) -> PurchasedItem? {
return status.filter( {$0.productID == productID } ).first
}
func getItemsAnync() {
// this will fill `purchasedItems` asynchronously
}
purchasedItems array will be filled with PurchasedItems, asynchronous, as the values of price, title and description come from the App Store.
Meanwhile, at another part of the interface, I am displaying buttons on a view, like this:
var buttonPackage1String:String {
let item = Packages.sharedInstance.getItem(Packages.Package.package1.rawValue)!
let string = """
\(umItem.localizedTitle) ( \(umItem.localizedPrice) ) \
\(umItem.localizedDescription) )
"""
return string
}
// inside var Body
Button(action: {
// purchase selected package
}) {
Text(buttonPackage1String)
.padding()
.background(Color.green)
.foregroundColor(.white)
.padding()
}
See my problem?
buttonPackage1String is built with title, description and price from an array called purchasedItems that is stored inside a singleton and that array is filled asynchronously, being initially empty.
So, at the time the view is displayed all values may be not retrieved yet but will, eventually.
Is there a way for the button is updated after the values are retrieved?
Calculable properties are not observed by SwiftUI. We have to introduce explicit state for this and update it once dependent dynamic data changed.
Something like,
#ObservedObject var packages: Packages
...
#State private var title: String = ""
...
Button(action: {
// purchase selected package
}) {
Text(title)
}
.onChange(of: packages.purchasedItems) { _ in
self.title = buttonPackage1String // << here !!
}
Below is a small pseudo code, I'm not sure it's suitable for your project but might give you some hint.
Packages.sharedInstance
.$purchasedItems //add $ to get the `Combine` Subject of variable
.compactMap { $0.firstWhere { $0.id == Packages.Package.package1.rawValue } } //Find the correct item and skip nil value
.map { "\($0.localizedTitle) ( \($0.localizedPrice) ) \ \($0.localizedDescription) )" }
.receive(on: RunLoop.main) // UI updates
.assign(to: \.buttonTitleString, on: self) //buttonTitleString is a #Published variable of your View model
.store(in: &cancellables) //Your cancellable collector
Combine framework knowledge is a must to start with SwiftUI.

Problem with mapping an array of objects in Swift

I have the following 2 class / structs:
class ConversationDetails {
var messages: [ChatMessage]?
var participants: [User]?
}
class User: Codable {
init (email: String) {
self.email = email
}
// system
var id: String?
var verifiedaccount: Int?
var rejected: Int?
...
}
I've further got the var conversationDetails = ConversationDetails () and I'm populating it with an API call. That all works fine.
I'd like to map the participants array inconversationDetailsand access the id property of each participant like so:
let recipient_ids = self.conversationDetails.participants.map( { (participant) -> String in
return participant.id
})
In my understanding, map iterates over the entire participants array, which is an array of User objects and I can access each item via participant.
However, I get Value of type '[User]' has no member 'id' for return participant.id.
Where is my misunderstanding?
Your participants var is optional, so you need to add question mark to access array. Without it, you try to call map on optional.
This is working code:
let recipientIds = conversationDetails.participants?.map( { (participant) -> String in
return participant.id
})
or shorter:
let recipientIds = conversationDetails.participants?.map { $0.id }
Also, you can use compactMap to remove nils from recipientIds array and have [String] array instead of [String?]:
let recipientIds = conversationDetails.participants?.compactMap { $0.id }
The first problem is that participants is optional so you need to add a ? when accessing it
conversationDetails.participants?
then when mapping you should use compactMap since id is also an optional property
let recipient_ids = conversationDetails.participants?.compactMap { $0.id }
Another variant is to not have an optional array but instead initialize it to an empty array. This is actually a much better way to handle collection properties because you can have a cleaner code by initializing them to an empty collection
var participants = [User]()
and then do
let recipient_ids = conversationDetails.participants.compactMap { $0.id }

Get and Set Array in Swift Class

I was build an IOS application, and somehow I have to do an aggregation in swift class, but every time I want to get the data, it always return an error. it seems like I the data always return a nil result.
I want to push the DoctorList member object (schedule) in the view controller class
I have create the object, and I also called the init() function. but, however, when I push the (or call) the init function for the DoctorList class and pass the array of ScheduleList, the content of the schedule member in doctorlist class will always be empty.
so when I try to get the schedule, it will return a nil result.
can every one tell me what I did wrong, because I already change the code but it still give a nil result.
I have two class like this
class DoctorList: NSObject {
var DoctorID: String?
var DoctorName: String?
var SpecialtyID: String?
var SpecialtyName: String?
var ImageURL: String?
var Schedule: [ScheduleList]?
init(_ DoctorID:String, _ DoctorName:String, _ SpecialtyID:String, _ SpecialtyName:String, _ ImageUrl:String, _ Schedule:[ScheduleList] ){
self.DoctorID = DoctorID
self.DoctorName = DoctorName
self.SpecialtyID = SpecialtyID
self.SpecialtyName = SpecialtyName
self.ImageURL = ImageUrl
for sc in Schedule {
self.Schedule?.append(ScheduleList(sc.DoctorID!, sc.DayName!, sc.FirstHour!, sc.LastHour!))
}
}
var getSchedule: [ScheduleList] {
get {
return self.Schedule!
}
}
and this one
class ScheduleList: NSObject {
var DoctorID: String?
var DayName: String?
var FirstHour: String?
var LastHour: String?
init(_ DoctorID:String, _ DayName:String, _ FirstHour:String, _ LastHour:String ){
self.DoctorID = DoctorID
self.DayName = DayName
self.FirstHour = FirstHour
self.LastHour = LastHour
}
the return value for the schedule was always empty
I'm sorry, could anyone give a suggestion how to make a global variable in swift?
You haven't initialized the Schedule array.
The append statement in the loop just never execute:
for sc in Schedule {
// self.Schedule is nil so anything come after the question mark does not run
self.Schedule?.append(ScheduleList(sc.DoctorID!, sc.DayName!, sc.FirstHour!, sc.LastHour!))
}
To fix it initialize your array before use:
self.Schedule = [ScheduleList]()
for sc in Schedule {
self.Schedule?.append(ScheduleList(sc.DoctorID!, sc.DayName!, sc.FirstHour!, sc.LastHour!))
}
Also, your code is a pain to read:
Optionals everywhere! You should decide what properties can and cannot be nil and get rid of the unnecessary ? and !
The convention in Swift is lowerCamelCase for variable names, and CamelCase for class names
No need to inherit from NSObject unless you want something from the ObjC world, whether working with ObjC or use KVO
Just noticed, recommendations from #CodeDifferent should be appropriate and helpful.
It is because you haven't initialised the Schedule in DoctorList
init(_ DoctorID:String, _ DoctorName:String, _ SpecialtyID:String, _ SpecialtyName:String, _ ImageUrl:String, _ Schedule:[ScheduleList] ){
self.DoctorID = DoctorID
self.DoctorName = DoctorName
self.SpecialtyID = SpecialtyID
self.SpecialtyName = SpecialtyName
self.ImageURL = ImageUrl
// Use this for init an empty array and append the content
self.Schedule = [ScheduleList]()
self.Schedule?.append(contentsOf: Schedule)
}
An example of the result:
let schedule = ScheduleList("scheduleId", "name", "1st", "last")
let doctorList = DoctorList("docId", "docName", "specId", "scpecName", "imgURL", [schedule])
if let list = doctorList.Schedule {
print("\(list[0].DoctorID)")
}