Post not yet marked as solved
I'm implementing (for the first time) a web service client in Swift using the Combine framework.
For a method that makes a single call to a web service and then closes the connection, what is considered best practice – to return a Future or a Publisher?
func getSomeThing() -> Future<SomeThing...> { }
func getSomeThing() -> AnyPublisher<SomeThing...> { }
In Flutter, a single async value would be a "Future" and multiple async values would be a "Stream". In the WWDC 2019 talk: "Introducing Combine" Apple presents a slide that hints at Futures as the async version of a single sync value. And Publishers as the async version of sync arrays.
I understand that Futures in fact are Publishers. And that there may be some subtle technical nuances to consider (greedy/lazy etc).
But thinking about the communicated intent, returning a Future seems to indicate a one hit wonder pipeline better than returning an AnyPublisher. But returning a Publisher seems easier and more in line with the framework, since it's pretty easy to eraseToAnyPublisher, for example from an URLSession.DataTaskPublisher, and just return that object.
Mapping a DataTaskPublisher pipeline to a Future dosen't seem to be straight forward at all.
Any best practice advise would be appreciated.
Post not yet marked as solved
I'm trying to update my SwiftUI view when system volume changes. Ultimately, my use case is to display a low volume warning overlay when the system volume is low. Right now, though, let's say I'm just trying to show the current volume in a Text label. I've tried using onReceive with a publisher on AVAudioSession.sharedInstance().outputVolume, as in below, but my onReceive block only fires once when the view appears, and is not called when the volume subsequently changes. Why does the below not work, and what is the best way to update SwiftUI views when volume changes?
struct Test: View {
@State var volume: Float = 0
var body: some View {
Text("current volume is \(volume)")
.onReceive(AVAudioSession.sharedInstance().publisher(for: \.outputVolume), perform: { value in
self.volume = value
})
}
}
Thanks!
Post not yet marked as solved
Is it possible to use the new http traffic instrument with combines dataTaskPublisher? How can I set a label to the task?
Post not yet marked as solved
In a project that uses the Combine Framework, the following error suddenly occurs:
Failed to build module 'Combine'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 5.5 (swiftlang-1300.0.24.14 clang-1300.0.25.10)', while this compiler is 'Apple Swift version 5.5 (swiftlang-1300.0.24.13 clang-1300.0.25.10)'). Please select a toolchain which matches the SDK.
I must confess that I do not understand what the problem is. Is this an Xcode problem or can I solve it myself?
Strangely enough, I was able to compile and run the project several times before without any problem.
Furthermore, the error is not displayed in my code, but in the package https://github.com/MaxDesiatov/XMLCoder which I included using cocoapods.
I'm using playground to experiment with Combine. I found this example on a blog. I expect it to create the Future publisher, wait a couple seconds then send the promise and complete.
import Foundation
import Combine
import SwiftUI
let future = Future<Int, Never> { promise in
print("Creating")
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
print("sending promise")
promise(.success(1))
}
}
future
.sink(receiveCompletion: { print("receiveCompletion:\($0)") },
receiveValue: { print("receiveValue:\($0)") })
print("end")
The output I expect:
Creating
end
sending promise
receiveValue: ...
receiveCompletion: ...
The output I get:
Creating
end
sending promise
I don't see an indication the promise was executed. What am I doing wrong?
Post not yet marked as solved
Unable to combine a project with dependencies that rely on combine. Getting an error saying unable to compile combine
This issue started with Xcode 13 beta 3 and still persists with beta 4 for me.
The project builds and runs fine in debug configuration, this error only happens when trying to build in release configuration.
This is happening with a dependency linked though SPM.
Not sure how to fix this
Post not yet marked as solved
Now I want to notify value change from delegate event to another func() and returns the new value with callback(). To do that, I tried to write test program as below. However, problem happened.
import SwiftUI
struct ContentView: View {
let cst = CombineSinkTest()
var body: some View {
Button(action: {
cst.sinkTest(stCount: 0){ sinkResult in
print("finish")
}
}){
Text("Push")
}
}
}
import Foundation
import Combine
final public class CombineSinkTest: NSObject{
var stringPublisher = PassthroughSubject<String?, Never>()
var cancellables = [AnyCancellable]()
private var globalNum = 0
var sinkDefinedFlag = false
func sinkTest(stCount: Int, completion: @escaping (Bool) -> Void){
var localCount = stCount
childSink(count: localCount){ childResult in
localCount += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.sinkTest(stCount: localCount){ stResult in
completion(true)
}
}
}
}
func childSink(count: Int, completion: @escaping (Bool) -> Void ){
globalNum += 1
let localNum = globalNum
print("globalNum: \(localNum)")
if(sinkDefinedFlag == false) {
self.stringPublisher
.sink(receiveCompletion: { print ("completion: \($0)") },
receiveValue: {
print ("received: \($0)")
print("local value: \(localNum)")
completion(true)
})
.store(in: &cancellables)
sinkDefinedFlag = true
}
(a) generateNotification()
}
(a) private func generateNotification() {
stringPublisher.send("")
}
}
(a) is test-purpose. (a)'s func is planned to be delegate event.
I want to let receiveValue: in .sink to use localCount number. However, localCount won't increase at all...
globalNum: 1
localNum in caller of .sink: 1
received: Optional("")
localNum in .sink: 1
globalNum: 2
localNum in caller of .sink: 2
received: Optional("")
localNum in .sink: 1
globalNum: 3
localNum in caller of .sink: 3
received: Optional("")
localNum in .sink: 1
globalNumber is increasing. And, localNum in func defining .sink. However, localCount in .sink closure won't increase. How should I change the code for passing updated localNum into .sink closure?
Sorry for not good English...
Best regards,
Post not yet marked as solved
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.
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.
Post not yet marked as solved
I am working on a library, a Swift package. We have quite a few properties on various classes that can change and we think the @Published property wrapper is a good way to annotate these properties as it offers a built-in way to work with SwiftUI and also Combine.
Many of our properties can change on background threads and we've noticed that we get a purple runtime issue when setting the value from a background thread. This is a bit problematic for us because the state did change on a background thread and we need to update it at that time. If we dispatch it to the main queue and update it on the next iteration, then our property state doesn't match what the user expects. Say they "load" or "start" something asynchronously, and that finishes, the status should report "loaded" or "started", but that's not the case if we dispatch it to the main queue because that property doesn't update until the next iteration of the run loop.
There also isn't any information in the documentation for @Published that suggests that you must update it on the main thread. I understand why SwiftUI wants it on the main thread, but this property wrapper is in the Combine framework. Also it seems like SwiftUI internally could ask to receive the published updates on the main queue and @Published shouldn't enforce a specific thread.
One thing we are thinking about doing is writing our own property wrapper, but that doesn't seem to be ideal for SwiftUI integration and it's one more property wrapper that users of our package would need to be educated about.
Any thoughts on direction? Is there anyway to break @Published from the main thread?
Post not yet marked as solved
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.
Post not yet marked as solved
Here's the model of the API that I'm using
// MARK: - Welcome
struct NOAAWeatherDecoder: Codable, Identifiable {
var id = UUID()
let type: String
let geometry: Geometry
let properties: Properties
enum CodingKeys: String, CodingKey {
case type, geometry, properties
}
}
// MARK: - Properties
struct Properties: Codable {
let updated: Date
let units, forecastGenerator: String
let generatedAt, updateTime: Date
let validTimes: String
let elevation: Elevation
let periods: [Period]
}
// MARK: - Elevation
struct Elevation: Codable {
let value: Double
let unitCode: String
}
// MARK: - Period
struct Period: Codable {
let number: Int
let name: String
let startTime, endTime: Date
let isDaytime: Bool
let temperature: Int
let temperatureUnit: TemperatureUnit
let temperatureTrend: JSONNull?
let windSpeed, windDirection: String
let icon: String
let shortForecast, detailedForecast: String
}
I am able to get upto periods, but nothing after that. Here's the part of the code where I'm running into the issue
ForEach(noaaWeatherData.weathernoaadata) { day in Text(day.properties.periods[0]) After this I'm not entirely sure. }
Thanks!
I am learning combine and one of the thing that is holding me back is the confusion of the semantic meaning of Cancellables. Why are they called Cancellables. I have tried to search for this answer on google but I didn’t find sufficient answers to satisfy my curiosity. Also, what is the advantage of using combine bindings over computed properties in SwiftUI. Thanks
Post not yet marked as solved
I have been working on this for a while and haven't found a solution. I have a populated public core data database (used readonly in the app) that I need to pull from on launch. This ONLY works with SwiftUI via @FetchRequest or FetchRequest -- NSFetchResultsController, attempting to do it with persistentCloudKitContainer.viewContext.perform {} doesn't fetch (all do work after relaunching the app).
The only thing that works is FetchRequest, but I need to do things in my model as soon as I get that data from CloudKit (create local data in response). How can I set, in my model, allItems to the wrappedValue property of the FetchRequest or @FetchRequest ? Since it is from cloudkit just setting the variable won't work, I need to use combine, but I can't wrap my head around how. If anyone has any suggestions -- I'd much appreciate the assistance.
class MyViewModel: ObservableObject {
typealias PublisherType = PassthroughSubject<MyViewModel, Never>
let objectWillChange = ObservableObjectPublisher()
let objectDidChange = ObservableObjectPublisher()
@Published var allItems: [Item] = []
struct MyView: View {
@FetchRequest var fetchRequest = FetchRequest<Item>(entity: Item.entity(), sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])
var body: some View {
List(fetchRequest.wrappedValue) { (item:Item) in
Text(item.name)
}
}
I have an app that uses Realm as a data storage solution and Combine as a declarative framework.
Ever since I upgraded to iOS 15, my app has become unusable because of what looks like a navigation bug. It works fine with iOS 14.
The gist of what I believe the bug is, is that when I navigate to a child view and the parent view is updated, the navigation jumps back to the parent view instead of staying in the child view (https://www.youtube.com/watch?v=c-PAPy-RCqg)
So in my example, ContentView loads data Realm and creates a list of DetailViews to display the data. When the DetailView is displayed, it writes some back to the Realm table.
I have a sample app demonstrating the problem here: https://github.com/skywalkerdude/navsample. Since it is using Cocoapods, make sure you open NavSample.xcworkspace and not NavSample.xcodeproj.
Post not yet marked as solved
I have two API call's for data and if openWeatherData fails it prevents planData.trainingdata from loading. Is there a way to workaround this so planData still loads?
List(planData.trainingdata) { index in
ForEach(openWeatherData.openWeatherData) { day in
...
}
}
Here's the API call for openWeather
class DownloadOpenWeatherData: ObservableObject {
@Published var openWeatherData: [OpenWeatherDecoder] = []
let locationManager = LocationManager()
var openWeatherCancellabes = Set<AnyCancellable>()
init() {
print("OpenWeather: loading init")
locationManager.startLoc()
getOpenWeatherData(weatherUrl: "https://api.openweathermap.org/data/2.5/onecall?lat=\(locationManager.getLat())&lon=\(locationManager.getLong())&exclude=hourly,minutely&appid")
locationManager.stopLoc()
}
func getOpenWeatherData(weatherUrl: String) {
guard let weatherUrl = URL(string: weatherUrl) else { return }
print("getOpenWeather url \(weatherUrl)")
URLSession.shared.dataTaskPublisher(for: weatherUrl)
.subscribe(on: DispatchQueue.global(qos: .background))
.receive(on: DispatchQueue.main)
.tryMap { (data, response) -> Data in
print(response)
guard
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
print("data \(data)")
return data
}
.decode(type: OpenWeatherDecoder.self, decoder: JSONDecoder())
.sink { (completion) in
print(completion)
} receiveValue: { (returnedWeatherData) in
self.openWeatherData = [returnedWeatherData]
print("returnedWeatherData \(returnedWeatherData)")
}
.store(in: &openWeatherCancellabes)
}
}
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
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?
Post not yet marked as solved
I am using a Combine URLSession to pull data from the Food Data Central API and the Data I am receiving is not being decoded with the JSON decoder. I know the Data is being received because I use the String(data:encoding: .utf8) as a debug print and I can see the downloaded data correctly in the console. I get an error message after the .decode completion failure that says "The data couldn't be read because it isn't in the correct format."
I am guessing I have to add something like the "encoder: utf8" statement in the .decode function. Or maybe transform the data in the .tryMap closure before returning. But I have searched the documentation and other sources and have not found anywhere that discusses this.
I am a fairly new to Swift (my first real app), I am hoping someone more-experienced can point me in the right direction. My code is as follows:
private func fdcSearch(searchFor searchText: String) {
let query = "https://api.nal.usda.gov/fdc/v1/foods/search?api_key=***&query=+Apple%20+Fuji"
let searchURL = "https://api.nal.usda.gov/fdc/v1/foods/search?"
let searchQuery = "&query="+searchString
print(searchURL+devData.apiKey+searchQuery)
// guard let url = URL(string: "https://api.nal.usda.gov/fdc/v1/foods/search?api_key=***&query=AppleFuji") else {
guard let url = URL(string: searchURL+devData.apiKey+searchQuery) else {
print("Guard error on url assignment") // debug statement
return
}
print("In fdcSearch") // debug statement
fdcSearchSubscription = URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .default))
.tryMap { (output) -> Data in
guard let response = output.response as? HTTPURLResponse, response.statusCode >= 200 && response.statusCode < 300 else {
print("bad server response") // debug statement
throw URLError(.badServerResponse)
}
print("got output") // debug statement
if let dataString = String(data: output.data, encoding: .utf8) { // debug statement
print("got dataString: \n\(dataString)") // debug statement
} // debug statement
return output.data
}
.receive(on: DispatchQueue.main)
.decode(type: [FDCFoodItem].self, decoder: JSONDecoder())
.sink { (completion) in
switch completion {
case .finished:
print("Completion finished") // debug statement
break
case .failure(let error):
print("Completion failed") // debug statement
print(error.localizedDescription)
}
} receiveValue: { [weak self] (returnedFoods) in
self?.foods = returnedFoods
print("returnedFoods: \(returnedFoods)") // debug statement
print("self?.foods: \(String(describing: self?.foods))") // debug statement
}
}
Any suggestions on how to handle this?
Post not yet marked as solved
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.