How can I map to a type in Combine? - swift

I have numerous pages where users may input information. They may input the fields with dates, numbers, or text.
I am trying to receive all changes in Combine and get their outputs as Encodable so I can easily upload the results to the network.
A String is Encodable, so I thought this would be easy but I cannot get this to work in Combine. I get a compiler error:
Cannot convert return expression of type 'Publishers.Map<Published.Publisher, Encodable>' to return type 'Published.Publisher'
There is a workaround where I add another property in SampleTextHandler that is #Published var userTextEncodable: Encodable but that's not what I want to do.
import Combine
protocol FieldResponseModifying {
var id: String { get }
var output: Published<Encodable>.Publisher { get }
}
struct SampleTextWrapper {
var output: Published<Encodable>.Publisher {
// Cannot convert return expression of type 'Publishers.Map<Published<String>.Publisher, Encodable>' to return type 'Published<Encodable>.Publisher'
handler.$userTextOutput.map { $0 as Encodable}
}
let id = UUID().uuidString
let handler = SampleTextHandler()
}
class SampleTextHandler {
#Published var userTextOutput = ""
init () { }
}

Combine uses generics heavily. For example, the type returned by your use of map is Publishers.Map<Published<Value>.Publisher, Encodable>. So you could declare your property like this:
var output: Publishers.Map<Published<Encodable>.Publisher, Encodable> {
handler.$userTextOutput.map { $0 as Encodable}
}
But now your property's type depends closely on how it is implemented. If you change the implementation, you'll have to change the type.
Instead, you should almost certainly use the “type eraser” AnyPublisher, like this:
var output: AnyPublisher<Encodable, Never> {
handler.$userTextOutput
.map { $0 as Encodable }
.eraseToAnyPublisher()
}
You're probably going to run into another issue down the line, due to your use of the Encodable existential. When you hit that, you'll want to post another question.

Related

Instance method 'drive' requires the types 'NotificationItem' and '[NotificationItem]' be equivalent

I have create a class called notification Item and parsing the data from model class RTVNotification
import Foundation
import RTVModel
public class NotificationItem: NSObject {
public var id: String
public var title: String
public var comment: String
public var publishStartDateString: String
init(id: String,
title: String,
comment: String,
publishStartDateString: String) {
self.id = id
self.title = title
self.comment = comment
self.publishStartDateString = publishStartDateString
super.init()
}
}
extension NotificationItem {
static func instantiate(with notification: RTVNotification) -> NotificationItem? {
return NotificationItem(
id: notification.id,
title: notification.title,
comment: notification.comment,
publishStartDateString: notification.publishStartDateString)
}
}
ViewModel
public class SettingsViewModel: ViewModel {
var item = [NotificationItem]()
public var fetchedNotifications: Driver<NotificationItem> = .empty()
public var apiErrorEvents: Driver<RTVAPIError> = .empty()
public var notificationCount: Driver<Int> = .empty()
public func bindNotificationEvents(with trigger: Driver<Void>) {
let webService: Driver<RTVInformationListWebService> = trigger
.map { RTVInformationListParameters() }
.webService()
let result = webService.request()
apiErrorEvents = Driver.merge(apiErrorEvents, result.error())
notificationCount = result.success().map {$0.informationList.maxCount }
fetchedNotifications =
result.success()
.map {$0.informationList.notifications}
-----> .map {NotificationItem.instantiate(with: $0)}
}
}
Getting an Error saying that Cannot convert value of type '[RTVNotification]' to expected argument type 'RTVNotification'
What can i do to solve this.
The purpose of the map() function is to iterate over the elements of an input array and apply a transform function to each of those elements. The transformed elements are added to a new output array that is returned by map(). It's important to understand that the length of the output array is the same length as the input array.
For example:
let inputArray = ["red", "white", "blue"]
let outputArray = inputArray.map { $0.count } // outputArray is [3, 5, 4]
In your code, you are calling:
result.success().map { $0.informationList.notifications }
I don't know RxSwift at all, so I'm going to go into wild speculation here.
First, I don't know exactly what result.success() returns, but the fact you can call map() on it implies result.success() returns an array (which is weird, but ok we'll go with it).
Second, we know the array returned by result.success() contains elements that have an informationList property, and the informationList property has a property called notifications. My guess is that notifications, being plural, means the notifications property type is an array, probably [RTVNotification].
So this code:
result.success().map { $0.informationList.notifications }
Transforms the success() array into a new array. Based on my assumption that notifications is of type [RTVNotification], and further assuming the success() array contains only one element, I would expect the result of
result.success().map { $0.informationList.notifications }
To be an array of type [[RTVNotification]], i.e. an array with one element, where that element is an array of RTVNotifications.
You then feed that [[RTVNotification]] array into another map() function:
.map { NotificationItem.instantiate(with: $0) }
Recall from the start of this answer that map() iterates over the elements of arrays. Since the input array to this map is [[RTVNotification]], its elements will be of type [RTVNotification]. That's what the $0 is in your call - [RTVNotification]. But the instantiate(with:) function takes an RTVNotification, not an array of RTVNotification, thus you get the error:
Cannot convert value of type '[RTVNotification]' to expected argument type 'RTVNotification'
So what can you do to fix it?
I would probably do something like this (you'll have to tailor it to your use case):
guard let successResponse = webService.request().success().first else {
print("no success response received")
return nil // basically report up an error here if not successful
}
// get the list of notifications, this will be type [RTVNotification]
let notifications = successResponse.informationList.notifications
// Now you can create an array of `[NotificationItem]` like you want:
let notificationItems = notifications.map { NotificationItem.instantiate(with: $0) }
// do something with notificationItems...
The caveat to the above is if you need to iterate over each element in the success() array, then you could do that like this:
let successResponses = webService.result().success()
// successNotifications is of type [[RTVNotification]]
let successNotifications = successResponses.map { $0.informationList.notifications }
// successItems will be of type [[NotificationItem]]
let successItems = successNotifications.map { notifications in
notifications.map { NotificationItem.instantiate(with: $0) }
}
In other words, in this last case, you get back an array that contains arrays of NotificationItem.
Your problem is here:
fetchedNotifications: Driver<NotificationItem> should be fetchedNotifications: Driver<[NotificationItem]> and the line .map {NotificationItem.instantiate(with: $0)} needs another map You are dealing with an Observable<Array<RTVNotification>>. You have a container type within a container type, so you need a map within a map:
.map { $0.map { NotificationItem.instantiate(with: $0) } }
When your types don't match, you need to change the types.
Other issues with your code...
Drivers, Observables, Subjects and Relays should never be defined with var, they should always be lets. Objects that subscribe to your properties before the bind is called will connect to the .empty() observables and never get any values. This is functional reactive programming, after all.
Your NotificationItem type should either be a struct or all it's properties should be `let's.
Be sure to read and understand #par's answer to this question. He wrote a really good explanation and it would be a shame to waste that knowledge transfer.

Referring to properties of containing class when using internal structs in swift

I'm refactoring a project to use MVVM and using protocols to ensure that my view models have a consistent structure. This works fine for defining public properties relating to input and output (which are based on internal structs) but defining actions in the same way is proving problemmatic as, currently, they are defined as closures which have to refer to view model properties. If I use the same approach as I have to input and output properties, I don't think I can access properties of the containing instance.
Example:
protocol ViewModelType {
associatedtype Input
associatedtype Output
associatedtype Action
}
final class MyViewModel: ViewModelType {
struct Input { var test: String }
struct Output { var result: String }
struct Action {
lazy var createMyAction: Action<String, Void> = { ... closure to generate Action which uses a MyViewModel property }
}
var input: Input
var output: Output
var action: Action
}
It's not a deal breaker if I can't do it, but I was curious as I can't see any way of getting access to the parent's properties.
Answer to your question
Let's begin with a note that createMyAction: Action<String, Void> refers to the type (struct) named Action as if it was a generic, but you have not declared it as such and will thus not work.
And to answer your question of the nested struct Action can refer its outer class MyViewModel - yes you can refer static properties, like this:
struct Foo {
struct Bar {
let biz = Foo.buz
}
static let buz = "buz"
}
let foobar = Foo.Bar()
print(foobar.biz)
But you should probably avoid such circular references. And I will omit any ugly hack that might be able to achive such a circular reference on non static properties (would probably involve mutable optional types). It is a code smell.
Suggestion for MVVM
Sounds like you would like to declare Action as a function? I'm using this protocol myself:
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
Originally inspired by SergDort's CleanArchitecture.
You can prepare an instance of input (containing Observables) from the UIViewController and call the transform function and then map the Output of transform (being Observabless) to update the GUI.
So this code assumes you have basic Reactive knowledge. As for Observables you can chose between RxSwift or ReactiveSwift - yes their names are similar.
If you are comfortable with Rx, it is an excellent way of achieving a nice MVVM architecture with simple async updates of the GUI. In the example below, you will find the type Driver which is documented here, but the short explanation is that is what you want to use for input from views and input to views, since it updates the views on the GUI thread and it is guaranteed to not error out.
CleanArchitecture contains e.g. PostsViewModel :
final class PostsViewModel: ViewModelType {
struct Input {
let trigger: Driver<Void>
let createPostTrigger: Driver<Void>
let selection: Driver<IndexPath>
}
struct Output {
let fetching: Driver<Bool>
let posts: Driver<[PostItemViewModel]>
let createPost: Driver<Void>
let selectedPost: Driver<Post>
let error: Driver<Error>
}
private let useCase: PostsUseCase
private let navigator: PostsNavigator
init(useCase: PostsUseCase, navigator: PostsNavigator) {
self.useCase = useCase
self.navigator = navigator
}
func transform(input: Input) -> Output {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
let posts = input.trigger.flatMapLatest {
return self.useCase.posts()
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriverOnErrorJustComplete()
.map { $0.map { PostItemViewModel(with: $0) } }
}
let fetching = activityIndicator.asDriver()
let errors = errorTracker.asDriver()
let selectedPost = input.selection
.withLatestFrom(posts) { (indexPath, posts) -> Post in
return posts[indexPath.row].post
}
.do(onNext: navigator.toPost)
let createPost = input.createPostTrigger
.do(onNext: navigator.toCreatePost)
return Output(fetching: fetching,
posts: posts,
createPost: createPost,
selectedPost: selectedPost,
error: errors)
}
}

In Swift, how to make a descendant of String? I get error "Inheritance from non-protocol, non-class type 'String'" when trying to do that

I want to have a function return a type which I can add a new function, but still can be generally recognized as String (still have all of String's methods and still can be received by any parameter that received String).
But when I try to derive a class from String, I get this error:
Inheritance from non-protocol, non-class type 'String'
Of course, I can instead use an extension to extend the existing String to add that function, but I felt that this will pollute the String with unnecessary and unrelated functions for general use.
For example, functions that I want to add might be like this:
class ImageUrl : String {
func getImage (callback: ((UIImage?)->Void)) { ... }
}
or like this:
class Base64 : String {
var image : UIImage { ... }
var data : Data { ... }
var string : String { ... }
}
Which will be confusing if I extend these functions to the main String type.
How can I do this in Swift? Or is there any workaround to this? Thanks.
You cannot inherit from String in Swift, because it is a struct. You can only add functionality by using an extension as you mentioned, but this will not let you use stored properties (computed properties as you wrote in the question are allowed).
However, a better approach for your need would be to use composition:
class Base64 {
let str: String
required init(value: String) {
self.str = value
}
}
Here you can add your desired functionality.
You could create a String extension using the fileprivate modifier to avoid polluting the global namespace. The functions would then only be available to code in the source file rather than the global namespace.
fileprivate extension String {
var image : UIImage { ... }
var data : Data { ... }
var string : String { ... }
}

Swift: Generic's type protocol not being recognized

Long time listener, first time caller.
I'm getting the following error:
Cannot convert value of type MyClass<Model<A>, OtherClass> to expected argument type MyClass<Protocol, OtherClass>
Despite the fact that MyClass<T> conforms to Protocol
I've attached a snippet that can be run in Playgrounds that resembles what I am actually trying to achieve.
protocol DisplayProtocol {
var value: String { get }
}
class DataBundle<T: CustomStringConvertible>: DisplayProtocol {
var data: T
var value: String {
return data.description
}
init(data: T) {
self.data = data
}
}
class Mapper<DisplayProtocol, Data> {
// ...
}
class MapperViewModel<Data> {
let mapper: Mapper<DisplayProtocol, Data>
init(mapper: Mapper<DisplayProtocol, Data>) {
self.mapper = mapper
}
}
let dataBundle = DataBundle<Int>(data: 100)
let mapper = Mapper<DataBundle<Int>, Bool>()
let viewModel = MapperViewModel<Bool>(mapper: mapper) // <- This fails w/error
Is this the expected behavior? If it is it feels like its breaking the contract of allowing me to have the DisplayProtocol as a type in Mapper.
This is caused by the fact that Swift generics are invariant in respect to their arguments. Thus MyClass<B> is not compatible with MyClass<A> even if B is compatible with A (subclass, protocol conformance, etc). So yes, unfortunately the behaviour is the expected one.
In your particular case, if you want to keep the current architecture, you might need to use protocols with associated types and type erasers.

Changing a struct with one type to another type

I have two structs with the same fields. What is the best way to merge them.
struct Type1{
var variable1:String?
var variable2:Double?
var variable3:String?
var notImporant:String?
}
struct Type2{
var variable1A:String?
var variable2A:String?
var variable3A:String!
}
What is the best way to convert type2 to type1? I am getting a return from an API and parsing it using codable but there are two different structs and I need to get one struct. The data is the same, it is just mapped differently in terms of types. Some of the structs have more info and others have less.
Just make a copy constructor in both structs like so:
struct Type2 {
var variable1A:String?
var variable2A:String?
var variable3A:String!
init(_ otherType: Type1) {
variable1A = otherType.variable1
variable2A = otherType.variable2
variable3A = otherType.variable3
}
}
You cannot cast two unrelated structs. What you can do is define a common Protocol for the two of them, and use them in places where you don't care which underlying object it is.
protocol CommonProtocol {
var variable1: String? { get }
var variable3: String? { get }
}
struct Type1: CommonProtocol {
var variable1:String?
var variable2:Double?
var variable3:String?
var notImporant:String?
}
struct Type2: CommonProtocol {
var variable1A:String?
var variable2A:String?
var variable3A:String!
}
Then, in whichever place you're currently stuck with a type1 instead of a type2, have that function just accept a CommonProtocol instead, and you can use either.
Note that, while both of your types have a variable2, one of them is a Double? while the other is a String?. There are a few different ways to approach that, which I leave to you. I just left it out of the protocol.
On another note, it's Swift standard to capitalize the names of structs (Type1, Type2). In certain instances, you can run into problems if you don't, so I suggest you do.