I'm using UIKit not SwiftUI. I found solutions which all in SwiftUI but not solved my problem.
I have a @Published object like:
@Published var searchText = ""
I'm using that @Published object for search functionality like in following function. Also, I'm trying to reach that function outside of corresponding class which is final class MainViewModel
final class MainViewModel {
@Published var searchText = ""
//subscribes to the searchText 'Publisher'
func searchTextManipulation(searchText: String) {
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] (text) in //(text) is always empty string ""
...
I want to use that function parameter searchText: String
as $searchText
to search with text come from another screen something like:
final class MainViewController {
let viewModel = MainViewModel()
@Published var searchText = ""
override func viewDidLoad() {
viewModel.searchTextManipulation($searchText: searchText) //but it's not possible.
}
}
Can you show me a workaround for solving that crucial part?
CodePudding user response:
Your code suggests that need to understand how Combine
works a little bit better.
When you put together a publisher with a set of operators:
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in doSomething(text) }
It's like you're putting together a set of pipes. You only have to put them together once. When they are assembled, and you put something into the input side, the pipes will transform the value and deliver it to the output side. If you subscribe to the output side, using sink
or assign
for example, then you can catch the transformed value and do something with it.
But you only need to build the pipeline once. After you build the pipeline, you also have to keep ahold of it.
Your searchTextManipulation
function is building a pipeline that is immediately destroyed when you leave the function. To prevent that you have to store a subscription (in the form of an AnyCancellable
). sink
and assign
return subscriptions and you should retain those to keep the pipeline from being destroyed.
An @Published
property creates a Publisher
, with the same name and a $
prefix, that emits a value when the property is initialized or changed. In other words, when you change searchText
the system will put the value into the input of a pipeline named $searchText
for you.
So a complete example of what it looks like you are trying to do is the playground below.
MainViewModel
has two published properties. One is searchText
and the other is searchResults
. We set up the model so that when searchText
changes (with a debounce and uniq) a new value of searchResults
is published.
In init
we build a pipeline that starts with $searchText
. Through the pipeline we transform a search string into an array of search results. (The idea that each step in a pipeline transforms a value from one thing to another is the model I use in my head to decide how to chain operators.)
The assign
at the end of the pipeline takes the transformed result and assigns it to the searchResults
property.
Note: To ensure our pipeline is not destroyed when init
ends, we have to capture and store the subscription done by assign
.
With that pipeline in place, whenever code changes searchText
the pipeline transform that value into an array of search results, and store the result in searchResults
. Since MainViewModel
owns the subscription, the pipeline will only be destroyed when the view model instance is destroyed.
So how do you use it? Well, the searchResults
get published to a pipeline named $searchResults
whenever they change. So all you have to do is listen that pipeline for changes.
In MainViewController
we want to listen for those changes. In viewDidLoad
we set up a short pipeline that starts with $searchResults
, drops the first result (it's sent when searchResults is initialized) and uses
sink` to print the results to the console.
Again note: we are constructing the pipeline once, keeping track of it by storing the subscription, and it will run automatically every time a new value is put into the seachResults
.
I added a function called userDoesSomething
that should be called whenever you want to kick off the search process.
userDoesSomething
assigns some text value that came from the user to the model'ssearchText
property.- Because it's @Published, the system sends the new value into the
$searchText
pipeline created by the view model'sinit
function. - The pipeline transforms the text into and array of search results and assigns the array to
searchResults
- Because
searchResults
is@Published
it sends the array into the pipeline called$searchResults
- The
MainViewController
created a pipeline to listen to$searchResults
and that pipeline runs with the new results. - That pipeline sends the value to a
sink
that simply prints it to the console.
The bit at the very end of the playground simulates a user sending a string with each of the letters in the English alphabet ('a'-'z') once every 0.2 seconds to userDoesSomething
. That kicks of the numbered sequence I described above and gives you something interesting to look at on the Playground console.
import UIKit
import Combine
import PlaygroundSupport
let TheItems = ["The", "Quick", "Brown", "Fox", "Jumped", "Over", "the", "Lazy", "Dogs"]
final class MainViewModel {
@Published var searchText = ""
@Published private(set) var searchResults = []
var subscription: AnyCancellable! = nil
init() {
self.subscription = $searchText
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.removeDuplicates()
.map(findItems)
.assign(to: \.searchResults, on: self)
}
}
private func findItems(searchText: String) -> [String] {
if let firstChar = searchText.lowercased().first {
return TheItems.filter { $0.lowercased().contains(firstChar) }
}
return []
}
final class MainViewController: UIViewController {
var searchResultsSubscription : AnyCancellable! = nil
let viewModel = MainViewModel()
override func loadView() {
self.view = UIView(frame: CGRect(x: 0,y: 0,width: 200,height: 200))
}
override func viewDidLoad() {
searchResultsSubscription = viewModel.$searchResults
.dropFirst()
.sink{
// do what you like here when search results change
debugPrint($0)
}
}
func userDoesSomething(generates searchText: String) {
viewModel.searchText = searchText;
}
}
let model = MainViewModel()
let controller = MainViewController()
PlaygroundSupport.PlaygroundPage.current.liveView = controller
(UInt8(ascii: "a")...UInt8(ascii:"z"))
.enumerated()
.publisher
.sink { index, char in
DispatchQueue.main.asyncAfter(deadline: .now() .milliseconds(200 * index)) {
controller.userDoesSomething(generates: String(UnicodeScalar(char)))
}
}
CodePudding user response:
Yes, you can pass a @Published
wrapper around, by changing the function signature to expect the projected value of the property wrapper:
func searchTextManipulation(searchText: Published<String>.Publisher) {
...
}
, and later in your viewDidLoad
just use the $
:
override func viewDidLoad() {
viewModel.searchTextManipulation(searchText: $searchText)
}
Everytime you write $someVar
, what the compiler does is to de-sugar it into _someVar.projectedValue
, and in case of Published
, the projected value is of type Published<T>.Publisher
. This is not something specific to SwiftUI, and it's part of the language, so you can freely use it in any parts of your code (assuming it makes sense, ofcourse).