If I want to subscribe to four @Published variables at the same time, I can do something like the following.
Publishers.CombineLatest4($variable0, $variable1, $variable2, $variable3)
I wonder if there is any solution to subscribing to more than four variables at the same time?
Muchos thankos
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Created
I'm trying to figure out how to use URLSession with the Combine framework.
I have a class that is to fetch data as follows.
import UIKit
import Combine
class APIClient: NSObject {
var cancellables = [AnyCancellable]()
@Published var models = [MyModel]()
func fetchData(urlStr: String) -> AnyPublisher<[MyModel], Never> {
guard let url = URL(string: urlStr) else {
let subject = CurrentValueSubject<[MyModel], Never>([])
return subject.eraseToAnyPublisher()
}
let subject = CurrentValueSubject<[MyModel], Never>(models)
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [MyModel].self, decoder: JSONDecoder())
.replaceError(with: [])
.sink { posts in
print("api client: \(posts.count)")
self.models = posts
}
.store(in: &cancellables)
return subject.eraseToAnyPublisher()
}
}
I then have a view model class that is to deliver data for my view controller as follows.
import Foundation
import Combine
class ViewModel: NSObject {
@IBOutlet var apiClient: APIClient!
var cancellables = Set<AnyCancellable>()
@Published var dataModels = [MyModel]()
func getGitData() -> AnyPublisher<[MyModel], Never> {
let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
let subject = CurrentValueSubject<[MyModel], Never>(dataModels)
apiClient.fetchData(urlStr: urlStr)
.sink { result in
print("view model: \(result.count)")
self.dataModels = result
}.store(in: &cancellables)
return subject.eraseToAnyPublisher()
}
}
My view controller has an IBOutlet of ViewModel.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
var cancellables = [AnyCancellable]()
@IBOutlet var viewModel: ViewModel!
// MARK: - IBOutlet
@IBOutlet weak var tableView: UITableView!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getGitData()
.sink { posts in
print("view controller: \(posts.count)")
}
.store(in: &cancellables)
}
}
If I run it, it seems that ViewModel returns 0 without waiting for APIClient to return data. And the view controller doesn't wait, either. What am I doing wrong? Can I do it without using the completion handler?
In case you need to know what MyModel is, it's a simple struct.
struct MyModel: Decodable {
let id: String
let type: String
}
Muchos thanks
I have the following lines of code to subscribe text changes over two text fields.
import UIKit
import Combine
class ViewController: UIViewController {
var cancellables = Set<AnyCancellable>()
@Published var userText: String = ""
@Published var passText: String = ""
// MARK: - IBOutlet
@IBOutlet var usernameTextField: UITextField!
@IBOutlet var passwordTextField: UITextField!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: usernameTextField)
.sink(receiveValue: { (result) in
if let myField = result.object as? UITextField {
if let text = myField.text {
self.userText = text
}
}
})
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: passwordTextField)
.sink(receiveValue: { (result) in
if let myField = result.object as? UITextField {
if let text = myField.text {
self.passText = text
}
}
})
.store(in: &cancellables)
$userText
.sink(receiveValue: { text in
print(text)
})
.store(in: &cancellables)
}
}
In the last several lines, I am printing the text change for userText. Does Combine allow me to observe two variables (userText, passText) at the same time so that I can plug them into a function? If yes, how?
Muchos Thankos.
I'm still a beginner in using Combine. I practice it on and off. Anyway, I have a view model to see changes in two text fields in my view controller as follows.
// ViewModel //
import Foundation
import Combine
class LoginViewModel {
var cancellable = [AnyCancellable]()
init(username: String, password: String) {
myUsername = username
myPassword = password
}
@Published var myUsername: String?
@Published var myPassword: String?
func validateUser() {
print("\(myUsername)")
print("\(myPassword)")
}
}
And my view controller goes as follows.
// ViewController //
import UIKit
import Combine
class HomeViewController: UIViewController {
// MARK: - Variables
var cancellable: AnyCancellable?
// MARK: - IBOutlet
@IBOutlet var usernameTextField: UITextField!
@IBOutlet var passwordTextField: UITextField!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
cancellable = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: usernameTextField)
.sink(receiveValue: { result in
if let textField = result.object as? UITextField {
if let text = textField.text {
let loginViewModel = LoginViewModel(username: text, password: "")
loginViewModel.validateUser()
}
}
})
}
}
So I use NSNotification as a publisher to see text changes over one of the text fields. And I cannot see text changes over two of them at the same time. Is there a better approach in seeing text changes over two text fields at the same time using Combine?
Muchos thankos.
Hello,
I'm trying to work out a simple example to fill table view data with Combine. The following is what I have.
import Foundation
struct MyModel: Decodable {
let id: String
let type: String
}
import UIKit
import Combine
class APIClient: NSObject {
var cancellable: AnyCancellable?
let sharedSession = URLSession.shared
func fetchData(urlStr: String, completion: @escaping ([MyModel]?) -> Void) {
guard let url = URL(string: urlStr) else {
return
}
let publisher = sharedSession.dataTaskPublisher(for: url)
cancellable = publisher.sink(receiveCompletion: { (completion) in
switch completion {
case .failure(let error):
print(error)
case .finished:
print("Success")
}
}, receiveValue: { (result) in
let decoder = JSONDecoder()
do {
let post = try decoder.decode([MyModel].self, from: result.data)
completion(post)
} catch let error as NSError {
print("\(error)")
completion(nil)
}
})
}
}
import Foundation
class ViewModel: NSObject {
@IBOutlet var apiClient: APIClient!
var dataModels = [MyModel]()
func getGitData(completion: @escaping () -> Void) {
let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
apiClient.fetchData(urlStr: urlStr) { (models) in
if let myModels = models {
self.dataModels = myModels.map { $0 }
}
completion()
}
}
}
import UIKit
import Combine
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// MARK: - Variables
var cancellable: AnyCancellable?
@IBOutlet var viewModel: ViewModel!
@Published var models = [MyModel]()
// MARK: - IBOutlet
@IBOutlet weak var tableView: UITableView!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getGitData {
self.models = self.viewModel.dataModels
}
cancellable = $models.sink(receiveValue: { (result) in
DispatchQueue.main.async {
[weak self] in
guard let strongSelf = self else { return }
strongSelf.tableView.reloadData()
}
})
}
// MARK: - TableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let dataModel = models[indexPath.row]
cell?.textLabel?.text = dataModel.id
cell?.detailTextLabel?.text = dataModel.type
return cell!
}
}
I'm not quite comfortable with the lines of code under my view controller (ViewController) in using Combine. How can I make them better? Muchos thankos.
Let me say that I have an IBOutlet object like
@IBOutlet weak var deleteButton: UIButton!
RxCocoa can make this button tap observable like
deleteButton.rx.tap
It doesn't look like Combine lets us observe a button tap. Am I right? I find one approach found at the following URL.
https://www.avanderlee.com/swift/custom-combine-publisher/
And Combine has no native approach? And you still have to use the IBAction?
Hola,
I have the following simple lines of code.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
var cancellable: AnyCancellable?
@Published var labelValue: String?
// MARK: - IBOutlet
@IBOutlet weak var textLabel: UILabel!
// MARK: - IBAction
@IBAction func actionTapped(_ sender: UIButton) {
labelValue = "Jim is missing"
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
cancellable = $labelValue
.receive(on: DispatchQueue.main)
.assign(to: \.text, on: textLabel)
}
}
I just wonder what is the point of specifying the main thread with .receive? If I comment out the receive line, the app will still run without a problem. Muchos thankos
import SwiftUI
struct ContentView: View {
@State var numbers = [2021, 9, 30]
var body: some View {
//let firstLocalYear = 2021
let firstLocalMonth = 9
let firstLocalDay = 24
let firstLastDay = 30
NavigationView {
List {
Section(header: Text("Current month")) {
ForEach(firstLocalDay ..< firstLastDay) { i in
HStack {
Text("\(firstLocalMonth)-\(i + 1)")
Spacer()
NavigationLink(
destination: TimeView(numbers: $numbers),
label: {
Text("")
})
}
}
}
}
}
}
}
struct TimeView: View {
@Binding var numbers: [Int]
var body: some View {
HStack {
Text(String(numbers[0]))
Text(String(numbers[1]))
Text(String(numbers[2]))
}
}
}
I have the lines of code above to list some rows of text. For now, numbers is a state variable that is pre-determined. This state variable is passed on to TimeView. Actually, I want to change this array depending on which row the user selects like
numbers = [firstLocalYear, firstLocalMonth, i]
where i comes from the ForEach thing. How can I change this array? Muchos thankos.
I have the following simple lines of code.
import SwiftUI
struct ContentView: View {
var users = ["Susan", "Kate", "Natalie", "Kimberly", "Taylor", "Sarah", "Nancy", "Katherine", "Nicole", "Linda", "Jane", "Mary", "Olivia", "Barbara"]
var body: some View {
List {
ForEach(users, id: \.self) { user in
Text(user)
}
}
}
}
So I'm just listing names. What I want to ask is what is id and what .self means. If I look up the doc under ForEach, it says the following.
Either the collection’s elements must conform to Identifiable or you need to provide an id parameter to the ForEachinitializer.
Does the compiler automatically generate a unique string like UUID for each element in the array or something? Can I somehow print the raw value of each id? Muchos thankos.
I'm playing with @EnvironmentObject to see how it works in SwiftUI. I have the main view (ContentView) where it says the user has not logged in yet. By letting the user tap a link, I want to make it such that they can log in by tapping a button.
class LoginMe: ObservableObject {
@Published var loggedIn = false
}
struct ContentView: View {
@StateObject var loginMe = LoginMe()
var body: some View {
if loginMe.loggedIn {
Text("Yes, I'm logged in")
} else {
NavigationView {
VStack {
Text("No, not logged in yet")
.padding(.vertical)
NavigationLink(destination: LoginView()) {
Text("Tap to log in")
}
}
.navigationBarTitle("User")
}
.environmentObject(loginMe)
}
}
}
struct LoginView: View {
@EnvironmentObject var loginMe: LoginMe
var body: some View {
/*
Toggle(isOn: $loginMe.loggedIn) {
Text("Log in")
}.padding(.horizontal)
*/
Button("Login") {
loginMe.loggedIn.toggle()
}
}
}
So far, when the user taps a button in the LoginView view, the screen goes back to ContentView and the navigation simply disappears. How can I change my code so that the status will change back and forth in in the LoginView view by tapping a button and then so that they can return to ContentView the navigation return button? I think the problem is that I need to use @State var in ContentView and @Binding var in LoginView. Things are kind of confusing. Muchos thankos.
I have the following lines of code for showing a list of friends.
import SwiftUI
struct ContentView: View {
@State var users = ["Susan", "Kate", "Natalie", "Kimberly", "Taylor", "Sarah", "Nancy", "Katherine", "Nicole", "Linda", "Jane", "Mary", "Olivia", "Barbara"]
@State var editMode = EditMode.inactive
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
Text(user)
}
}
.navigationBarTitle("Friends")
.environment(\.editMode, $editMode)
.navigationBarItems(leading: Button("Edit", action: {
if self.editMode == .active {
self.editMode = .inactive
} else {
self.editMode = .active
}
}))
}
}
}
If you see the code at the bottom, I have four lines just in order to change the value of editMode. Does SwiftUI have something like
showDetails.toggle()
where showDetails is a Boolean variable? Muchos thankos.
I have the following lines of code to work with a list of strings.
import SwiftUI
struct ContentView: View {
@State private var users = ["George", "Kenny", "Susan", "Natalie"]
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
Text(user)
}
.onDelete(perform: delete)
}
.navigationBarTitle("My family")
.toolbar {
EditButton()
}
}
}
func delete(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}
}
Now, I'm doing the following out of curiosity. Now, I have a button in naviationBarItems. And I wonder if I can turn on and off the edit feature of the list with the button?
struct ContentView: View {
@State private var users = ["George", "Kenny", "Susan", "Natalie"]
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
Text(user)
}
}
.navigationBarTitle("My family")
.navigationBarItems(trailing:
Button(action: {
print("Edit button pressed...")
}) {
Text("Edit")
}
)
}
}
}
Muchos thankos.
I just want to show a simple navigation title like the following.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.edgesIgnoringSafeArea(.all)
Text("Hello")
}
.navigationTitle("GGG")
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(false)
}
}
}
And I get a bunch of mumbo jumbo auto-layout warnings (Unable to simultaneously satisfy constraints...) in Console. If I comment out the navigationTitle line, I won't get them. I have never seen those messages in showing a navigation title when writing code with UIKit. What am I doing wrong? Muchos thankos
I'm trying to understand how Combine works. The following is my sample code.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
var cancellable: AnyCancellable?
// MARK: - IBAction
@IBAction func buttonTapped(_ sender: UIButton) {
currentValueSubject.send(20)
}
// MARK: - Life cycle
var currentValueSubject = CurrentValueSubject<Int, Never>(1)
override func viewDidLoad() {
super.viewDidLoad()
let cancellable = currentValueSubject
.sink { value in
print("New value: \(value)")
}
currentValueSubject.send(5)
currentValueSubject.send(10)
//currentValueSubject.send(completion: .finished)
currentValueSubject.send(15)
//cancellable.cancel()
}
}
If I run it with the iPhone simulator, I get
New value: 1
New value: 5
New value: 10
New value: 15
If I tap the button, the app won't get a new value. I suppose that's because the subscription is cancelled at the end of viewDidLoad? If so, why does it get cancelled? I don't quite see a practical side of Combine's Subject. When is it useful? Thanks.
I have the following lines of code in practicing Combine.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
var cancellable: AnyCancellable?
@Published var segmentNumber: Int = 0
// MARK: - IBOutlet
@IBOutlet weak var actionButton: UIButton!
// MARK: - IBAction
@IBAction func segmentChanged(_ sender: UISegmentedControl) {
segmentNumber = sender.selectedSegmentIndex
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
cancellable = $segmentNumber.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: actionButton)
}
}
I get an error at .assign that says
Value of type 'UIView?' has no member 'isEnabled'
What am I doing wrong? Thank you.